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:
parent
8956b273f0
commit
cf5470ae58
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ca.uhn.fhir.context;
|
||||
|
||||
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
|
@ -8,6 +9,7 @@ import org.apache.commons.lang3.builder.ToStringBuilder;
|
|||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseExtension;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
@ -18,6 +20,7 @@ import java.util.HashMap;
|
|||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
|
@ -56,6 +59,7 @@ public class RuntimeSearchParam {
|
|||
private final RuntimeSearchParamStatusEnum myStatus;
|
||||
private final String myUri;
|
||||
private final Map<String, List<IBaseExtension<?, ?>>> myExtensions = new HashMap<>();
|
||||
private final Map<String, String> myUpliftRefchains = new HashMap<>();
|
||||
private final ComboSearchParamType myComboSearchParamType;
|
||||
private final List<Component> myComponents;
|
||||
private IPhoneticEncoder myPhoneticEncoder;
|
||||
|
@ -148,7 +152,7 @@ public class RuntimeSearchParam {
|
|||
/**
|
||||
* Sets user data - This can be used to store any application-specific data
|
||||
*/
|
||||
public RuntimeSearchParam addExtension(String theKey, IBaseExtension theValue) {
|
||||
public RuntimeSearchParam addExtension(String theKey, IBaseExtension<?, ?> theValue) {
|
||||
List<IBaseExtension<?, ?>> valuesList = myExtensions.computeIfAbsent(theKey, k -> new ArrayList<>());
|
||||
valuesList.add(theValue);
|
||||
return this;
|
||||
|
@ -276,6 +280,40 @@ public class RuntimeSearchParam {
|
|||
return retVal;
|
||||
}
|
||||
|
||||
public void addUpliftRefchain(@Nonnull String theCode, @Nonnull String theElementName) {
|
||||
myUpliftRefchains.put(theCode, theElementName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this search parameter have an uplift refchain definition for the given code?
|
||||
* See the HAPI FHIR documentation for a description of how uplift refchains work.
|
||||
*
|
||||
* @since 6.6.0
|
||||
*/
|
||||
public boolean hasUpliftRefchain(String theCode) {
|
||||
return myUpliftRefchains.containsKey(theCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of all codes associated with uplift refchains for this search parameter.
|
||||
* See the HAPI FHIR documentation for a description of how uplift refchains work.
|
||||
*
|
||||
* @since 6.6.0
|
||||
*/
|
||||
public Set<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 {
|
||||
ACTIVE,
|
||||
DRAFT,
|
||||
|
|
|
@ -23,12 +23,16 @@ package ca.uhn.fhir.util;
|
|||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseDatatype;
|
||||
import org.hl7.fhir.instance.model.api.IBaseExtension;
|
||||
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -291,4 +295,32 @@ public class ExtensionUtil {
|
|||
public static boolean equals(IBaseExtension<?,?> theLeftExtension, IBaseExtension<?,?> theRightExtension) {
|
||||
return TerserUtil.equals(theLeftExtension, theRightExtension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an extension, looks for the first child extension with the given URL of {@literal theChildExtensionUrl}
|
||||
* and a primitive datatype value, and returns the String version of that value. E.g. if the
|
||||
* value is a FHIR boolean, it would return the string "true" or "false. If the extension
|
||||
* has no value, or the value is not a primitive datatype, or the URL is not found, the method
|
||||
* will return {@literal null}.
|
||||
*
|
||||
* @param theExtension The parent extension. Must not be null.
|
||||
* @param theChildExtensionUrl The child extension URL. Must not be null or blank.
|
||||
* @since 6.6.0
|
||||
*/
|
||||
public static <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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,6 +140,22 @@ public class HapiExtensions {
|
|||
*/
|
||||
public static final String EXTENSION_SUBSCRIPTION_CROSS_PARTITION = "https://smilecdr.com/fhir/ns/StructureDefinition/subscription-cross-partition";
|
||||
|
||||
/**
|
||||
* This extension is used for "uplifted refchains" on search parameters. See the
|
||||
* HAPI FHIR documentation for an explanation of how these work.
|
||||
*/
|
||||
public static final String EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN = "https://smilecdr.com/fhir/ns/StructureDefinition/searchparameter-uplift-refchain";
|
||||
/**
|
||||
* This extension is used for "uplifted refchains" on search parameters. See the
|
||||
* HAPI FHIR documentation for an explanation of how these work.
|
||||
*/
|
||||
public static final String EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE = "code";
|
||||
/**
|
||||
* This extension is used for "uplifted refchains" on search parameters. See the
|
||||
* HAPI FHIR documentation for an explanation of how these work.
|
||||
*/
|
||||
public static final String EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_ELEMENT_NAME = "element-name";
|
||||
|
||||
/**
|
||||
* Non instantiable
|
||||
*/
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.util;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
|
|
|
@ -22,6 +22,11 @@ package org.hl7.fhir.instance.model.api;
|
|||
|
||||
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 {
|
||||
|
||||
List<T> getExtension();
|
||||
|
|
|
@ -119,6 +119,8 @@ ca.uhn.fhir.jpa.dao.BaseStorageDao.deleteResourceNotExisting=Not deleted, resour
|
|||
ca.uhn.fhir.jpa.dao.BaseStorageDao.deleteResourceAlreadyDeleted=Not deleted, resource {0} was already deleted.
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidSearchParameter=Unknown search parameter "{0}" for resource type "{1}". Valid search parameters for this search are: {2}
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidSortParameter=Unknown _sort parameter value "{0}" for resource type "{1}" (Note: sort parameters values must use a valid Search Parameter). Valid values for this search are: {2}
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidSortParameterTooManyChains=Invalid _sort expression, can not chain more than once in a sort expression: {0}
|
||||
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.updateWithNoId=Can not update resource of type {0} as it has no ID
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidBundleTypeForStorage=Unable to store a Bundle resource on this server with a Bundle.type value of: {0}. Note that if you are trying to perform a FHIR 'transaction' or 'batch' operation you should POST the Bundle resource to the Base URL of the server, not to the '/Bundle' endpoint.
|
||||
ca.uhn.fhir.rest.api.PatchTypeEnum.missingPatchContentType=Missing or invalid content type for PATCH operation
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-bom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>HAPI FHIR BOM</name>
|
||||
|
||||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-cli</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../../hapi-deployable-pom</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -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`."
|
|
@ -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."
|
|
@ -1,12 +1,15 @@
|
|||
# JPA Server Search
|
||||
|
||||
## Limitations
|
||||
This page describes features and limitations for performing [FHIR searches](https://hl7.org/fhir/search.html) on the JPA server.
|
||||
|
||||
The HAPI FHIR JPA Server fully implements most [FHIR search](https://www.hl7.org/fhir/search.html) operations for most versions of FHIR. However, there are some known limitations of the current implementation. Here is a partial list of search functionality that is not currently supported in HAPI FHIR:
|
||||
# Limitations
|
||||
|
||||
The HAPI FHIR JPA Server fully implements most [FHIR search](https://hl7.org/fhir/search.html) operations for most versions of FHIR. However, there are some known limitations of the current implementation. Here is a partial list of search functionality that is not currently supported in HAPI FHIR:
|
||||
|
||||
### Chains within _has
|
||||
|
||||
Chains within _has are not currently supported for performance reasons. For example, this search is not currently supported
|
||||
|
||||
```http
|
||||
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.
|
||||
|
||||
|
||||
<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.
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -999,7 +999,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
|||
entity.setDeleted(null);
|
||||
|
||||
// TODO: is this IF statement always true? Try removing it
|
||||
if (thePerformIndexing || ((ResourceTable) theEntity).getVersion() == 1) {
|
||||
if (thePerformIndexing || theEntity.getVersion() == 1) {
|
||||
|
||||
newParams = new ResourceIndexedSearchParams();
|
||||
|
||||
|
@ -1013,6 +1013,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
|||
}
|
||||
|
||||
failIfPartitionMismatch(theRequest, entity);
|
||||
|
||||
mySearchParamWithInlineReferencesExtractor.populateFromResource(requestPartitionId, newParams, theTransactionDetails, entity, theResource, existingParams, theRequest, thePerformIndexing);
|
||||
|
||||
changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
|
||||
|
|
|
@ -414,6 +414,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
jpaPid.setAssociatedResourceId(entity.getIdType(myFhirContext));
|
||||
myIdHelperService.addResolvedPidToForcedId(jpaPid, theRequestPartitionId, getResourceName(), entity.getTransientForcedId(), null);
|
||||
theTransactionDetails.addResolvedResourceId(jpaPid.getAssociatedResourceId(), jpaPid);
|
||||
theTransactionDetails.addResolvedResource(jpaPid.getAssociatedResourceId(), theResource);
|
||||
|
||||
// Pre-cache the match URL
|
||||
if (theMatchUrl != null) {
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.jpa.dao.data;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceSearchUrlEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.jpa.search;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.jpa.dao.data.IResourceSearchUrlDao;
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.jpa.search;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.api.svc.ISearchUrlJobMaintenanceSvc;
|
||||
import ca.uhn.fhir.jpa.model.sched.HapiJob;
|
||||
import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs;
|
||||
|
|
|
@ -77,6 +77,7 @@ import ca.uhn.fhir.rest.param.HasParam;
|
|||
import ca.uhn.fhir.rest.param.NumberParam;
|
||||
import ca.uhn.fhir.rest.param.QuantityParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.param.SpecialParam;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParamModifier;
|
||||
|
@ -129,6 +130,7 @@ import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOperation;
|
|||
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate;
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_HAS;
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_ID;
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.split;
|
||||
|
||||
|
@ -229,15 +231,103 @@ public class QueryStack {
|
|||
|
||||
}
|
||||
|
||||
public void addSortOnResourceLink(String theResourceName, String theParamName, boolean theAscending) {
|
||||
public void addSortOnResourceLink(String theResourceName, String theReferenceTargetType, String theParamName, String theChain, boolean theAscending) {
|
||||
BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
|
||||
ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = mySqlBuilder.createReferencePredicateBuilder(this);
|
||||
|
||||
Condition pathPredicate = resourceLinkPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName, new ArrayList<>());
|
||||
Condition pathPredicate = resourceLinkPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName);
|
||||
|
||||
addSortCustomJoin(firstPredicateBuilder, resourceLinkPredicateBuilder, pathPredicate);
|
||||
|
||||
if (isBlank(theChain)) {
|
||||
mySqlBuilder.addSortNumeric(resourceLinkPredicateBuilder.getColumnTargetResourceId(), theAscending, myUseAggregate);
|
||||
return;
|
||||
}
|
||||
|
||||
String targetType = null;
|
||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
||||
if (theReferenceTargetType != null) {
|
||||
targetType = theReferenceTargetType;
|
||||
} else if (param.getTargets().size() > 1) {
|
||||
throw new InvalidRequestException(Msg.code(2287) + "Unable to sort on a chained parameter from '" + theParamName + "' as this parameter has multiple target types. Please specify the target type.");
|
||||
} else if (param.getTargets().size() == 1) {
|
||||
targetType = param.getTargets().iterator().next();
|
||||
}
|
||||
|
||||
if (isBlank(targetType)) {
|
||||
throw new InvalidRequestException(Msg.code(2288) + "Unable to sort on a chained parameter from '" + theParamName + "' as this parameter as this parameter does not define a target type. Please specify the target type.");
|
||||
}
|
||||
|
||||
RuntimeSearchParam targetSearchParameter = mySearchParamRegistry.getActiveSearchParam(targetType, theChain);
|
||||
if (targetSearchParameter == null) {
|
||||
Collection<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) {
|
||||
|
@ -276,16 +366,22 @@ public class QueryStack {
|
|||
}
|
||||
|
||||
private void addSortCustomJoin(BaseJoiningPredicateBuilder theFromJoiningPredicateBuilder, BaseJoiningPredicateBuilder theToJoiningPredicateBuilder, Condition theCondition) {
|
||||
addSortCustomJoin(theFromJoiningPredicateBuilder.getResourceIdColumn(), theToJoiningPredicateBuilder, theCondition);
|
||||
}
|
||||
|
||||
private void addSortCustomJoin(DbColumn theFromDbColumn, BaseJoiningPredicateBuilder theToJoiningPredicateBuilder, Condition theCondition) {
|
||||
ComboCondition onCondition = mySqlBuilder.createOnCondition(
|
||||
theFromJoiningPredicateBuilder.getResourceIdColumn(),
|
||||
theFromDbColumn,
|
||||
theToJoiningPredicateBuilder.getResourceIdColumn()
|
||||
);
|
||||
|
||||
if (theCondition != null) {
|
||||
onCondition.addCondition(theCondition);
|
||||
}
|
||||
|
||||
mySqlBuilder.addCustomJoin(
|
||||
SelectQuery.JoinType.LEFT_OUTER,
|
||||
theFromJoiningPredicateBuilder.getTable(),
|
||||
theFromDbColumn.getTable(),
|
||||
theToJoiningPredicateBuilder.getTable(),
|
||||
onCondition);
|
||||
}
|
||||
|
@ -385,7 +481,7 @@ public class QueryStack {
|
|||
ourLog.error("Cannot create missing parameter query for a composite parameter.");
|
||||
return null;
|
||||
} else if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
|
||||
if (isEligibleForContainedResourceSearch(theParams.getQueryParameterTypes())) {
|
||||
if (isEligibleForEmbeddedChainedResourceSearch(theParams.getResourceType(), theParams.getParamName(), theParams.getQueryParameterTypes()).supportsUplifted()) {
|
||||
ourLog.error("Cannot construct missing query parameter search for ContainedResource REFERENCE search.");
|
||||
return null;
|
||||
}
|
||||
|
@ -572,6 +668,7 @@ public class QueryStack {
|
|||
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
|
||||
return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder);
|
||||
}
|
||||
|
||||
public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
|
||||
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
|
||||
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
|
||||
|
@ -958,220 +1055,19 @@ public class QueryStack {
|
|||
mySqlBuilder.getSelect().addGroupings(firstPredicateBuilder.getResourceIdColumn());
|
||||
}
|
||||
|
||||
private 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 (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,
|
||||
public Condition createPredicateReferenceForEmbeddedChainedSearchResource(@Nullable DbColumn theSourceJoinColumn,
|
||||
String theResourceName, RuntimeSearchParam theSearchParam,
|
||||
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
|
||||
EnumSet<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes = EnumSet.copyOf(myReusePredicateBuilderTypes);
|
||||
if (wantChainedAndNormal) {
|
||||
myReusePredicateBuilderTypes.clear();
|
||||
|
||||
UnionQuery union = new UnionQuery(SetOperationQuery.Type.UNION_ALL);
|
||||
}
|
||||
|
||||
ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor();
|
||||
chainExtractor.deriveChains(theResourceName, theSearchParam, theList);
|
||||
|
@ -1181,15 +1077,30 @@ public class QueryStack {
|
|||
for (List<ChainElement> nextChain : chains.keySet()) {
|
||||
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 (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) {
|
||||
SearchQueryBuilder builder = mySqlBuilder.newChildSqlBuilder();
|
||||
SearchQueryBuilder builder;
|
||||
if (wantChainedAndNormal) {
|
||||
builder = mySqlBuilder.newChildSqlBuilder();
|
||||
} else {
|
||||
builder = mySqlBuilder;
|
||||
}
|
||||
|
||||
DbColumn previousJoinColumn = null;
|
||||
|
||||
// Create a reference link predicate to the subselect for every link but the last one
|
||||
// Create a reference link predicates to the subselect for every link but the last one
|
||||
for (String nextLink : nextReferenceLink) {
|
||||
// We don't want to call createPredicateReference() here, because the whole point is to avoid the recursion.
|
||||
// TODO: Are we missing any important business logic from that method? All tests are passing.
|
||||
|
@ -1211,37 +1122,60 @@ public class QueryStack {
|
|||
theRequestPartitionId,
|
||||
builder);
|
||||
|
||||
if (wantChainedAndNormal) {
|
||||
builder.addPredicate(containedCondition);
|
||||
|
||||
union.addQueries(builder.getSelect());
|
||||
} else {
|
||||
predicates.add(containedCondition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InCondition inCondition;
|
||||
Condition retVal;
|
||||
if (wantChainedAndNormal) {
|
||||
|
||||
if (theSourceJoinColumn == null) {
|
||||
inCondition = new InCondition(mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union);
|
||||
retVal = new InCondition(mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union);
|
||||
} else {
|
||||
//-- 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
|
||||
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.
|
||||
// This is a bit excruciating to extend beyond three references. Do we want to find a way to automate this someday?
|
||||
// Note: the first element in each chain is assumed to be discrete. This may need to change when we add proper support for `_contained`
|
||||
if (nextChain.size() == 1) {
|
||||
// discrete -> discrete
|
||||
if (theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN) {
|
||||
// If !theWantChainedAndNormal that means we're only processing refchains
|
||||
// so the discrete -> contained case is the only one that applies
|
||||
updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes);
|
||||
}
|
||||
|
||||
// discrete -> contained
|
||||
RuntimeSearchParam firstParamDefinition = leafNodes.iterator().next().getParamDefinition();
|
||||
updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(),
|
||||
leafNodes
|
||||
.stream()
|
||||
.map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParameterName()))
|
||||
// When we're handling discrete->contained the differences between search
|
||||
// parameters don't matter. E.g. if we're processing "subject.name=foo"
|
||||
// the name could be Patient:name or Group:name but it doesn't actually
|
||||
// matter that these are different since in this case both of these end
|
||||
// up being an identical search in the string table for "subject.name".
|
||||
.map(t -> t.withParam(firstParamDefinition))
|
||||
.collect(Collectors.toSet()));
|
||||
} else if (nextChain.size() == 2) {
|
||||
// discrete -> discrete -> discrete
|
||||
|
@ -1359,7 +1293,7 @@ public class QueryStack {
|
|||
theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder);
|
||||
break;
|
||||
case REFERENCE:
|
||||
containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, StringUtils.isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, theQualifiers,
|
||||
containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, theQualifiers,
|
||||
theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder);
|
||||
break;
|
||||
case HAS:
|
||||
|
@ -1465,11 +1399,15 @@ public class QueryStack {
|
|||
|
||||
List<Condition> andPredicates = new ArrayList<>();
|
||||
for (List<? extends IQueryParameterType> nextAndParams : theList) {
|
||||
if ( ! checkHaveTags(nextAndParams, theParamName)) { continue; }
|
||||
if (!checkHaveTags(nextAndParams, theParamName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Triple<String, String, String>> tokens = Lists.newArrayList();
|
||||
boolean paramInverted = populateTokens(tokens, nextAndParams);
|
||||
if (tokens.isEmpty()) { continue; }
|
||||
if (tokens.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Condition tagPredicate;
|
||||
BaseJoiningPredicateBuilder join;
|
||||
|
@ -1529,14 +1467,18 @@ public class QueryStack {
|
|||
for (IQueryParameterType nextParamUncasted : theParams) {
|
||||
if (nextParamUncasted instanceof TokenParam) {
|
||||
TokenParam nextParam = (TokenParam) nextParamUncasted;
|
||||
if (isNotBlank(nextParam.getValue())) { return true; }
|
||||
if (isNotBlank(nextParam.getValue())) {
|
||||
return true;
|
||||
}
|
||||
if (isNotBlank(nextParam.getSystem())) {
|
||||
throw new TokenParamFormatInvalidRequestException(Msg.code(1218), theParamName, nextParam.getValueAsQueryToken(myFhirContext));
|
||||
}
|
||||
}
|
||||
|
||||
UriParam nextParam = (UriParam) nextParamUncasted;
|
||||
if (isNotBlank(nextParam.getValue())) { return true; }
|
||||
if (isNotBlank(nextParam.getValue())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -1747,10 +1689,18 @@ public class QueryStack {
|
|||
break;
|
||||
case REFERENCE:
|
||||
for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
|
||||
if (isEligibleForContainedResourceSearch(nextAnd)) {
|
||||
andPredicates.add(createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
|
||||
} else {
|
||||
|
||||
// Handle Search Parameters where the name is a full chain
|
||||
// (e.g. SearchParameter with name=composition.patient.identifier)
|
||||
if (handleFullyChainedParameter(theSourceJoinColumn, theResourceName, theParamName, theRequest, theRequestPartitionId, andPredicates, nextAnd)) {
|
||||
break;
|
||||
}
|
||||
|
||||
EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum = isEligibleForEmbeddedChainedResourceSearch(theResourceName, theParamName, nextAnd);
|
||||
if (embeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY) {
|
||||
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId));
|
||||
} else {
|
||||
andPredicates.add(createPredicateReferenceForEmbeddedChainedSearchResource(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId, embeddedChainedSearchModeEnum));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -1829,14 +1779,100 @@ public class QueryStack {
|
|||
return toAndPredicate(andPredicates);
|
||||
}
|
||||
|
||||
private boolean isEligibleForContainedResourceSearch(List<? extends IQueryParameterType> nextAnd) {
|
||||
return myStorageSettings.isIndexOnContainedResources() &&
|
||||
nextAnd.stream()
|
||||
/**
|
||||
* This method handles the case of Search Parameters where the name/code
|
||||
* in the SP is a full chain expression. Normally to handle an expression
|
||||
* like <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)
|
||||
.map(t -> ((ReferenceParam) t).getChain())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
// Chains on _has can't be indexed for contained searches - At least not yet. It's not clear to me if we ever want to support this, it would be really hard to do.
|
||||
.anyMatch(t->!t.startsWith(PARAM_HAS + ":"));
|
||||
.filter(t -> !t.startsWith(PARAM_HAS + ":"))
|
||||
.anyMatch(t -> {
|
||||
if (indexOnContainedResources) {
|
||||
return true;
|
||||
}
|
||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceType, theParameterName);
|
||||
return param != null && param.hasUpliftRefchain(t);
|
||||
});
|
||||
|
||||
if (haveUpliftCandidates) {
|
||||
if (indexOnContainedResources) {
|
||||
return EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN;
|
||||
}
|
||||
return EmbeddedChainedSearchModeEnum.UPLIFTED_ONLY;
|
||||
} else {
|
||||
return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
|
||||
|
@ -1851,7 +1887,6 @@ public class QueryStack {
|
|||
mySqlBuilder.addPredicate(predicate);
|
||||
}
|
||||
|
||||
|
||||
// expand out the pids
|
||||
public void addPredicateEverythingOperation(String theResourceName, List<String> theTypeSourceResourceNames, Long... theTargetPids) {
|
||||
ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null);
|
||||
|
@ -1859,6 +1894,13 @@ public class QueryStack {
|
|||
mySqlBuilder.addPredicate(predicate);
|
||||
}
|
||||
|
||||
public IQueryParameterType toParameterType(RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) {
|
||||
IQueryParameterType qp = toParameterType(theParam);
|
||||
|
||||
qp.setValueAsQueryToken(myFhirContext, theParam.getName(), theQualifier, theValueAsQueryToken);
|
||||
return qp;
|
||||
}
|
||||
|
||||
private IQueryParameterType toParameterType(RuntimeSearchParam theParam) {
|
||||
|
||||
IQueryParameterType qp;
|
||||
|
@ -1890,13 +1932,255 @@ public class QueryStack {
|
|||
case URI:
|
||||
qp = new UriParam();
|
||||
break;
|
||||
case HAS:
|
||||
case REFERENCE:
|
||||
qp = new ReferenceParam();
|
||||
break;
|
||||
case SPECIAL:
|
||||
qp = new SpecialParam();
|
||||
break;
|
||||
case HAS:
|
||||
default:
|
||||
throw new InvalidRequestException(Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported.");
|
||||
}
|
||||
return qp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #isEligibleForEmbeddedChainedResourceSearch(String, String, List) for an explanation of the values in this enum
|
||||
*/
|
||||
enum EmbeddedChainedSearchModeEnum {
|
||||
|
||||
UPLIFTED_ONLY(true),
|
||||
UPLIFTED_AND_REF_JOIN(true),
|
||||
REF_JOIN_ONLY(false);
|
||||
|
||||
private final boolean mySupportsUplifted;
|
||||
|
||||
EmbeddedChainedSearchModeEnum(boolean theSupportsUplifted) {
|
||||
mySupportsUplifted = theSupportsUplifted;
|
||||
}
|
||||
|
||||
public boolean supportsUplifted() {
|
||||
return mySupportsUplifted;
|
||||
}
|
||||
}
|
||||
|
||||
private final static class ChainElement {
|
||||
private final String myResourceType;
|
||||
private final String mySearchParameterName;
|
||||
private final String myPath;
|
||||
|
||||
public ChainElement(String theResourceType, String theSearchParameterName, String thePath) {
|
||||
this.myResourceType = theResourceType;
|
||||
this.mySearchParameterName = theSearchParameterName;
|
||||
this.myPath = thePath;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return myResourceType;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return myPath;
|
||||
}
|
||||
|
||||
public String getSearchParameterName() {
|
||||
return mySearchParameterName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ChainElement that = (ChainElement) o;
|
||||
return myResourceType.equals(that.myResourceType) && mySearchParameterName.equals(that.mySearchParameterName) && myPath.equals(that.myPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(myResourceType, mySearchParameterName, myPath);
|
||||
}
|
||||
}
|
||||
|
||||
private class ReferenceChainExtractor {
|
||||
private final Map<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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -93,6 +93,7 @@ import ca.uhn.fhir.util.StringUtil;
|
|||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.healthmarketscience.sqlbuilder.Condition;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
|
@ -125,6 +126,7 @@ import java.util.Optional;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.countMatches;
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
@ -748,41 +750,111 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
|
||||
} else {
|
||||
|
||||
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(myResourceName, theSort.getParamName());
|
||||
RuntimeSearchParam param = null;
|
||||
|
||||
/*
|
||||
* If we have a sort like _sort=subject.name and we have an
|
||||
* uplifted refchain for that combination we can do it more efficiently
|
||||
* by using the index associated with the uplifted refchain. In this case,
|
||||
* we need to find the actual target search parameter (corresponding
|
||||
* to "name" in this example) so that we know what datatype it is.
|
||||
*/
|
||||
String paramName = theSort.getParamName();
|
||||
if (myStorageSettings.isIndexOnUpliftedRefchains()) {
|
||||
String[] chains = StringUtils.split(paramName, '.');
|
||||
if (chains.length == 2) {
|
||||
|
||||
// Given: Encounter?_sort=Patient:subject.name
|
||||
String referenceParam = chains[0]; // subject
|
||||
String referenceParamTargetType = null; // Patient
|
||||
String targetParam = chains[1]; // name
|
||||
|
||||
int colonIdx = referenceParam.indexOf(':');
|
||||
if (colonIdx > -1) {
|
||||
referenceParamTargetType = referenceParam.substring(0, colonIdx);
|
||||
referenceParam = referenceParam.substring(colonIdx + 1);
|
||||
}
|
||||
RuntimeSearchParam outerParam = mySearchParamRegistry.getActiveSearchParam(myResourceName, referenceParam);
|
||||
if (outerParam == null) {
|
||||
throwInvalidRequestExceptionForUnknownSortParameter(myResourceName, referenceParam);
|
||||
}
|
||||
|
||||
if (outerParam.hasUpliftRefchain(targetParam)) {
|
||||
for (String nextTargetType : outerParam.getTargets()) {
|
||||
if (referenceParamTargetType != null && !referenceParamTargetType.equals(nextTargetType)) {
|
||||
continue;
|
||||
}
|
||||
RuntimeSearchParam innerParam = mySearchParamRegistry.getActiveSearchParam(nextTargetType, targetParam);
|
||||
if (innerParam != null) {
|
||||
param = innerParam;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int colonIdx = paramName.indexOf(':');
|
||||
String referenceTargetType = null;
|
||||
if (colonIdx > -1) {
|
||||
referenceTargetType = paramName.substring(0, colonIdx);
|
||||
paramName = paramName.substring(colonIdx + 1);
|
||||
}
|
||||
|
||||
int dotIdx = paramName.indexOf('.');
|
||||
String chainName = null;
|
||||
if (param == null && dotIdx > -1) {
|
||||
chainName = paramName.substring(dotIdx + 1);
|
||||
paramName = paramName.substring(0, dotIdx);
|
||||
if (chainName.contains(".")) {
|
||||
String msg = myContext
|
||||
.getLocalizer()
|
||||
.getMessageSanitized(BaseStorageDao.class, "invalidSortParameterTooManyChains", paramName + "." + chainName);
|
||||
throw new InvalidRequestException(Msg.code(2286) + msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (param == null) {
|
||||
String msg = myContext.getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSortParameter", theSort.getParamName(), getResourceName(), mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(getResourceName()));
|
||||
throw new InvalidRequestException(Msg.code(1194) + msg);
|
||||
param = mySearchParamRegistry.getActiveSearchParam(myResourceName, paramName);
|
||||
}
|
||||
|
||||
if (param == null) {
|
||||
throwInvalidRequestExceptionForUnknownSortParameter(getResourceName(), paramName);
|
||||
}
|
||||
|
||||
if (isNotBlank(chainName) && param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
|
||||
throw new InvalidRequestException(Msg.code(2285) + "Invalid chain, " + paramName + " is not a reference SearchParameter");
|
||||
}
|
||||
|
||||
switch (param.getParamType()) {
|
||||
case STRING:
|
||||
theQueryStack.addSortOnString(myResourceName, theSort.getParamName(), ascending);
|
||||
theQueryStack.addSortOnString(myResourceName, paramName, ascending);
|
||||
break;
|
||||
case DATE:
|
||||
theQueryStack.addSortOnDate(myResourceName, theSort.getParamName(), ascending);
|
||||
theQueryStack.addSortOnDate(myResourceName, paramName, ascending);
|
||||
break;
|
||||
case REFERENCE:
|
||||
theQueryStack.addSortOnResourceLink(myResourceName, theSort.getParamName(), ascending);
|
||||
theQueryStack.addSortOnResourceLink(myResourceName, referenceTargetType, paramName, chainName, ascending);
|
||||
break;
|
||||
case TOKEN:
|
||||
theQueryStack.addSortOnToken(myResourceName, theSort.getParamName(), ascending);
|
||||
theQueryStack.addSortOnToken(myResourceName, paramName, ascending);
|
||||
break;
|
||||
case NUMBER:
|
||||
theQueryStack.addSortOnNumber(myResourceName, theSort.getParamName(), ascending);
|
||||
theQueryStack.addSortOnNumber(myResourceName, paramName, ascending);
|
||||
break;
|
||||
case URI:
|
||||
theQueryStack.addSortOnUri(myResourceName, theSort.getParamName(), ascending);
|
||||
theQueryStack.addSortOnUri(myResourceName, paramName, ascending);
|
||||
break;
|
||||
case QUANTITY:
|
||||
theQueryStack.addSortOnQuantity(myResourceName, theSort.getParamName(), ascending);
|
||||
theQueryStack.addSortOnQuantity(myResourceName, paramName, ascending);
|
||||
break;
|
||||
case COMPOSITE:
|
||||
List<RuntimeSearchParam> compositeList = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, param);
|
||||
if (compositeList == null) {
|
||||
throw new InvalidRequestException(Msg.code(1195) + "The composite _sort parameter " + theSort.getParamName() + " is not defined by the resource " + myResourceName);
|
||||
throw new InvalidRequestException(Msg.code(1195) + "The composite _sort parameter " + paramName + " is not defined by the resource " + myResourceName);
|
||||
}
|
||||
if (compositeList.size() != 2) {
|
||||
throw new InvalidRequestException(Msg.code(1196) + "The composite _sort parameter " + theSort.getParamName()
|
||||
throw new InvalidRequestException(Msg.code(1196) + "The composite _sort parameter " + paramName
|
||||
+ " must have 2 composite types declared in parameter annotation, found "
|
||||
+ compositeList.size());
|
||||
}
|
||||
|
@ -796,7 +868,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
case SPECIAL:
|
||||
case HAS:
|
||||
default:
|
||||
throw new InvalidRequestException(Msg.code(1197) + "This server does not support _sort specifications of type " + param.getParamType() + " - Can't serve _sort=" + theSort.getParamName());
|
||||
throw new InvalidRequestException(Msg.code(1197) + "This server does not support _sort specifications of type " + param.getParamType() + " - Can't serve _sort=" + paramName);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -806,6 +878,12 @@ public class SearchBuilder implements ISearchBuilder<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) {
|
||||
|
||||
switch (theParamType) {
|
||||
|
|
|
@ -282,8 +282,8 @@ public class ResourceLinkPredicateBuilder
|
|||
return QueryParameterUtils.toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
|
||||
}
|
||||
|
||||
public Condition createPredicateSourcePaths(String theResourceName, String theParamName, List<String> theQualifiers) {
|
||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
|
||||
public Condition createPredicateSourcePaths(String theResourceName, String theParamName) {
|
||||
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, Collections.emptyList());
|
||||
return createPredicateSourcePaths(pathsToMatch);
|
||||
}
|
||||
|
||||
|
@ -627,56 +627,12 @@ public class ResourceLinkPredicateBuilder
|
|||
type.setValueAsQueryToken(getFhirContext(), theParamName, qualifier, resourceId);
|
||||
chainValue = type;
|
||||
} else {
|
||||
chainValue = toParameterType(param, qualifier, resourceId);
|
||||
chainValue = myQueryStack.toParameterType(param, qualifier, resourceId);
|
||||
}
|
||||
|
||||
return chainValue;
|
||||
}
|
||||
|
||||
private IQueryParameterType toParameterType(RuntimeSearchParam theParam) {
|
||||
IQueryParameterType qp;
|
||||
switch (theParam.getParamType()) {
|
||||
case DATE:
|
||||
qp = new DateParam();
|
||||
break;
|
||||
case NUMBER:
|
||||
qp = new NumberParam();
|
||||
break;
|
||||
case QUANTITY:
|
||||
qp = new QuantityParam();
|
||||
break;
|
||||
case STRING:
|
||||
qp = new StringParam();
|
||||
break;
|
||||
case TOKEN:
|
||||
qp = new TokenParam();
|
||||
break;
|
||||
case COMPOSITE:
|
||||
List<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
|
||||
private InvalidRequestException newInvalidTargetTypeForChainException(String theResourceName, String theParamName, String theTypeValue) {
|
||||
String searchParamName = theResourceName + ":" + theParamName;
|
||||
|
@ -684,13 +640,6 @@ public class ResourceLinkPredicateBuilder
|
|||
return new InvalidRequestException(msg);
|
||||
}
|
||||
|
||||
private IQueryParameterType toParameterType(RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) {
|
||||
IQueryParameterType qp = toParameterType(theParam);
|
||||
|
||||
qp.setValueAsQueryToken(getFhirContext(), theParam.getName(), theQualifier, theValueAsQueryToken);
|
||||
return qp;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private InvalidRequestException newInvalidResourceTypeException(String theResourceType) {
|
||||
String msg = getFhirContext().getLocalizer().getMessageSanitized(SearchCoordinatorSvcImpl.class, "invalidResourceType", theResourceType);
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.jpa.util;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
|
@ -71,6 +72,7 @@ public class QueryParameterUtils {
|
|||
public static final int DEFAULT_SYNC_SIZE = 250;
|
||||
|
||||
private static final BidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> ourCompareOperationToParamPrefix;
|
||||
public static final Condition[] EMPTY_CONDITION_ARRAY = new Condition[0];
|
||||
|
||||
static {
|
||||
DualHashBidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> compareOperationToParamPrefix = new DualHashBidiMap<>();
|
||||
|
@ -89,13 +91,16 @@ public class QueryParameterUtils {
|
|||
|
||||
@Nullable
|
||||
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) {
|
||||
return null;
|
||||
} else if (andPredicates.size() == 1) {
|
||||
return andPredicates.get(0);
|
||||
} else {
|
||||
return ComboCondition.and(andPredicates.toArray(new Condition[0]));
|
||||
return ComboCondition.and(andPredicates.toArray(EMPTY_CONDITION_ARRAY));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,7 +112,7 @@ public class QueryParameterUtils {
|
|||
} else if (orPredicates.size() == 1) {
|
||||
return orPredicates.get(0);
|
||||
} else {
|
||||
return ComboCondition.or(orPredicates.toArray(new Condition[0]));
|
||||
return ComboCondition.or(orPredicates.toArray(EMPTY_CONDITION_ARRAY));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -255,8 +255,10 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP
|
|||
@Override
|
||||
public String toString() {
|
||||
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
b.append("resourceType", getResourceType());
|
||||
b.append("paramName", getParamName());
|
||||
b.append("resourceId", getResourcePid());
|
||||
b.append("hashIdentity", getHashIdentity());
|
||||
b.append("hashNormalizedPrefix", getHashNormalizedPrefix());
|
||||
b.append("valueNormalized", getValueNormalized());
|
||||
b.append("partitionId", getPartitionId());
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.jpa.model.entity;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Model
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
|
|
|
@ -111,6 +111,7 @@ public class StorageSettings {
|
|||
private Set<String> myAutoVersionReferenceAtPaths = Collections.emptySet();
|
||||
private Map<String, Set<String>> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap();
|
||||
private boolean myRespectVersionsForSearchIncludes;
|
||||
private boolean myIndexOnUpliftedRefchains = false;
|
||||
private boolean myIndexOnContainedResources = false;
|
||||
private boolean myIndexOnContainedResourcesRecursively = false;
|
||||
private boolean myAllowMdmExpansion = false;
|
||||
|
@ -1060,6 +1061,28 @@ public class StorageSettings {
|
|||
myRespectVersionsForSearchIncludes = theRespectVersionsForSearchIncludes;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, "Uplifted Refchains" will be enabled. This feature causes
|
||||
* HAPI FHIR to generate indexes for stored resources that include the current
|
||||
* value of the target of a chained reference, such as "Encounter?subject.name".
|
||||
*
|
||||
* @since 6.6.0
|
||||
*/
|
||||
public boolean isIndexOnUpliftedRefchains() {
|
||||
return myIndexOnUpliftedRefchains;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, "Uplifted Refchains" will be enabled. This feature causes
|
||||
* HAPI FHIR to generate indexes for stored resources that include the current
|
||||
* value of the target of a chained reference, such as "Encounter?subject.name".
|
||||
*
|
||||
* @since 6.6.0
|
||||
*/
|
||||
public void setIndexOnUpliftedRefchains(boolean theIndexOnUpliftedRefchains) {
|
||||
myIndexOnUpliftedRefchains = theIndexOnUpliftedRefchains;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should indexing and searching on contained resources be enabled on this server.
|
||||
* This may have performance impacts, and should be enabled only if it is needed. Default is <code>false</code>.
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -41,10 +41,12 @@ import ca.uhn.fhir.rest.api.Constants;
|
|||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import ca.uhn.fhir.util.FhirTerser;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import ca.uhn.fhir.util.StringUtil;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import ca.uhn.fhir.util.bundle.BundleEntryParts;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
@ -54,12 +56,14 @@ import org.apache.commons.text.StringTokenizer;
|
|||
import org.fhir.ucum.Pair;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseEnumeration;
|
||||
import org.hl7.fhir.instance.model.api.IBaseExtension;
|
||||
import org.hl7.fhir.instance.model.api.IBaseReference;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.hl7.fhir.r5.model.Base;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
|
@ -179,7 +183,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
@Override
|
||||
public SearchParamSet<PathAndRef> extractResourceLinks(IBaseResource theResource, boolean theWantLocalReferences) {
|
||||
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() {
|
||||
|
@ -233,6 +237,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
extractor = createSpecialExtractor(theResource.getIdElement().getResourceType());
|
||||
break;
|
||||
case COMPOSITE:
|
||||
case HAS:
|
||||
default:
|
||||
throw new UnsupportedOperationException(Msg.code(503) + "Type " + theSearchParam.getParamType() + " not supported for extraction");
|
||||
}
|
||||
|
@ -265,10 +270,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
}
|
||||
|
||||
@Override
|
||||
public SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource) {
|
||||
public SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
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) {
|
||||
|
@ -554,9 +558,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
}
|
||||
|
||||
@Override
|
||||
public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource) {
|
||||
public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
IExtractor<BaseResourceIndexedSearchParam> extractor = createTokenExtractor(theResource);
|
||||
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.TOKEN, false);
|
||||
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.TOKEN, false, theParamsToIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -589,10 +593,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
}
|
||||
|
||||
@Override
|
||||
public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource) {
|
||||
public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
String resourceTypeName = toRootTypeName(theResource);
|
||||
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) {
|
||||
|
@ -609,9 +613,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
}
|
||||
|
||||
@Override
|
||||
public SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource) {
|
||||
public SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
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) {
|
||||
|
@ -634,9 +638,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
}
|
||||
|
||||
@Override
|
||||
public SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource) {
|
||||
public SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
IExtractor<ResourceIndexedSearchParamDate> extractor = createDateExtractor(theResource);
|
||||
return extractSearchParams(theResource, extractor, DATE, false);
|
||||
return extractSearchParams(theResource, extractor, DATE, false, theParamsToIndex);
|
||||
}
|
||||
|
||||
private IExtractor<ResourceIndexedSearchParamDate> createDateExtractor(IBaseResource theResource) {
|
||||
|
@ -650,9 +654,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
}
|
||||
|
||||
@Override
|
||||
public SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource) {
|
||||
public SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
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) {
|
||||
|
@ -685,16 +689,16 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
}
|
||||
|
||||
@Override
|
||||
public SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource) {
|
||||
public SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
IExtractor<ResourceIndexedSearchParamQuantity> extractor = createQuantityUnnormalizedExtractor(theResource);
|
||||
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false);
|
||||
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false, theParamsToIndex);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource) {
|
||||
public SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
IExtractor<ResourceIndexedSearchParamQuantityNormalized> extractor = createQuantityNormalizedExtractor(theResource);
|
||||
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false);
|
||||
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false, theParamsToIndex);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
@ -764,10 +768,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
}
|
||||
|
||||
@Override
|
||||
public SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource) {
|
||||
public SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
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) {
|
||||
|
@ -830,7 +834,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
try {
|
||||
allValues = allValuesFunc.get();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString());
|
||||
throw new InternalErrorException(Msg.code(504) + msg, e);
|
||||
}
|
||||
|
@ -1319,7 +1322,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
* This is the only way you could actually specify a FhirPath expression for those
|
||||
* prior to 6.2.0 so this isn't a breaking change.
|
||||
*/
|
||||
<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<>();
|
||||
|
||||
Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
|
||||
|
@ -1331,6 +1334,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!theParamsToIndex.equals(ISearchParamExtractor.ALL_PARAMS) && !theParamsToIndex.contains(nextSpDef.getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// See the method javadoc for an explanation of this
|
||||
if (startsWith(nextSpDef.getPath(), "Resource.")) {
|
||||
continue;
|
||||
|
@ -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
|
||||
public interface IValueExtractor {
|
||||
|
||||
|
@ -1781,6 +1813,8 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
@Override
|
||||
public void extract(SearchParamSet<PathAndRef> theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences) {
|
||||
if (theValue instanceof IBaseResource) {
|
||||
myPathAndRef = new PathAndRef(theSearchParam.getName(), thePath, (IBaseResource) theValue);
|
||||
theParams.add(myPathAndRef);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
|
|||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public interface IResourceLinkResolver {
|
||||
|
||||
|
@ -35,16 +36,34 @@ public interface IResourceLinkResolver {
|
|||
* so that we can create indexed links between resources, and so that we can validate that the target actually
|
||||
* exists in cases where we need to check that.
|
||||
* <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 theSourceResourceName
|
||||
* @param theSourceResourceName The resource type for the resource containing the reference
|
||||
* @param thePathAndRef The path and reference
|
||||
* @param theRequest The incoming request, if any
|
||||
* @param theTransactionDetails
|
||||
* @param theTransactionDetails The current TransactionDetails object
|
||||
*/
|
||||
IResourceLookup findTargetResource(@Nonnull RequestPartitionId theRequestPartitionId, String theSourceResourceName, PathAndRef thePathAndRef, RequestDetails theRequest, TransactionDetails theTransactionDetails);
|
||||
|
||||
/**
|
||||
* This method resolves the target of a reference found within a resource that is being created/updated. We do this
|
||||
* so that we can create indexed links between resources, and so that we can validate that the target actually
|
||||
* exists in cases where we need to check that.
|
||||
* <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);
|
||||
|
||||
}
|
||||
|
|
|
@ -44,31 +44,66 @@ import java.util.Set;
|
|||
|
||||
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> extractSearchParamSpecial(IBaseResource theResource);
|
||||
|
||||
SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource, Set<String> theParamsToIndex);
|
||||
SearchParamSet<ResourceIndexedComboStringUnique> extractSearchParamComboUnique(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);
|
||||
|
||||
|
|
|
@ -20,17 +20,21 @@ package ca.uhn.fhir.jpa.searchparam.extractor;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseReference;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
public class PathAndRef {
|
||||
|
||||
private final String myPath;
|
||||
private final IBaseReference myRef;
|
||||
private final IBaseResource myResource;
|
||||
private final String mySearchParamName;
|
||||
private final boolean myCanonical;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* Constructor for a reference
|
||||
*/
|
||||
public PathAndRef(String theSearchParamName, String thePath, IBaseReference theRef, boolean theCanonical) {
|
||||
super();
|
||||
|
@ -38,6 +42,31 @@ public class PathAndRef {
|
|||
myPath = thePath;
|
||||
myRef = theRef;
|
||||
myCanonical = theCanonical;
|
||||
myResource = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a resource (this is expected to be rare, only really covering
|
||||
* cases like the path Bundle.entry.resource)
|
||||
*/
|
||||
public PathAndRef(String theSearchParamName, String thePath, IBaseResource theResource) {
|
||||
super();
|
||||
mySearchParamName = theSearchParamName;
|
||||
myPath = thePath;
|
||||
myRef = null;
|
||||
myCanonical = false;
|
||||
myResource = theResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that this will generally be null, it is only used for cases like
|
||||
* indexing {@literal Bundle.entry.resource}. If this is populated, {@link #getRef()}
|
||||
* will be null and vice versa.
|
||||
*
|
||||
* @since 6.6.0
|
||||
*/
|
||||
public IBaseResource getResource() {
|
||||
return myResource;
|
||||
}
|
||||
|
||||
public boolean isCanonical() {
|
||||
|
@ -52,8 +81,23 @@ public class PathAndRef {
|
|||
return myPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is populated, {@link #getResource()} will be null, and vice versa.
|
||||
*/
|
||||
public IBaseReference getRef() {
|
||||
return myRef;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
b.append("paramName", mySearchParamName);
|
||||
if (myRef != null && myRef.getReferenceElement() != null) {
|
||||
b.append("ref", myRef.getReferenceElement().getValue());
|
||||
}
|
||||
b.append("path", myPath);
|
||||
b.append("resource", myResource);
|
||||
b.append("canonical", myCanonical);
|
||||
return b.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,15 +133,15 @@ public final class ResourceIndexedSearchParams {
|
|||
theEntity.setResourceLinks(myLinks);
|
||||
}
|
||||
|
||||
public void updateSpnamePrefixForIndexedOnContainedResource(String theContainingType, String theSpnamePrefix) {
|
||||
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myNumberParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myQuantityParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myQuantityNormalizedParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myDateParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myUriParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myTokenParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myStringParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myCoordsParams, theSpnamePrefix);
|
||||
public void updateSpnamePrefixForIndexOnUpliftedChain(String theContainingType, String theSpnamePrefix) {
|
||||
updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myNumberParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myQuantityParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myQuantityNormalizedParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myDateParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myUriParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myTokenParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myStringParams, theSpnamePrefix);
|
||||
updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myCoordsParams, theSpnamePrefix);
|
||||
}
|
||||
|
||||
public void updateSpnamePrefixForLinksOnContainedResource(String theSpNamePrefix) {
|
||||
|
@ -176,7 +176,7 @@ public final class ResourceIndexedSearchParams {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateSpnamePrefixForIndexedOnContainedResource(String theContainingType, Collection<? extends BaseResourceIndexedSearchParam> theParams, @Nonnull String theSpnamePrefix) {
|
||||
private void updateSpnamePrefixForIndexOnUpliftedChain(String theContainingType, Collection<? extends BaseResourceIndexedSearchParam> theParams, @Nonnull String theSpnamePrefix) {
|
||||
|
||||
for (BaseResourceIndexedSearchParam param : theParams) {
|
||||
param.setResourceType(theContainingType);
|
||||
|
|
|
@ -75,7 +75,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
|
|||
public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
|
||||
return () -> {
|
||||
ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path));
|
||||
return myFhirPathEngine.evaluate((Base) theResource, parsed);
|
||||
return myFhirPathEngine.evaluate(theResource, (Base) theResource, (Base) theResource, (Base) theResource, parsed);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -98,13 +98,13 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
|
|||
}
|
||||
|
||||
|
||||
private static class SearchParamExtractorR4HostServices implements FHIRPathEngine.IEvaluationContext {
|
||||
private class SearchParamExtractorR4HostServices implements FHIRPathEngine.IEvaluationContext {
|
||||
|
||||
private final Map<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
@Override
|
||||
public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -135,6 +135,10 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
|
|||
|
||||
@Override
|
||||
public Base resolveReference(Object theAppContext, String theUrl, Base theRefContext) throws FHIRException {
|
||||
Base retVal = resolveResourceInBundleWithPlaceholderId(theAppContext, theUrl);
|
||||
if (retVal != null) {
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/*
|
||||
* When we're doing resolution within the SearchParamExtractor, if we want
|
||||
|
@ -144,7 +148,6 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
|
|||
* Encounter.patient.where(resolve() is Patient)
|
||||
*/
|
||||
IdType url = new IdType(theUrl);
|
||||
Base retVal = null;
|
||||
if (isNotBlank(url.getResourceType())) {
|
||||
|
||||
retVal = myResourceTypeToStub.get(url.getResourceType());
|
||||
|
|
|
@ -74,7 +74,7 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements
|
|||
public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
|
||||
return () -> {
|
||||
ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path));
|
||||
return myFhirPathEngine.evaluate((Base) theResource, parsed);
|
||||
return myFhirPathEngine.evaluate(theResource, (Base) theResource, (Base) theResource, (Base) theResource, parsed);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -97,13 +97,13 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements
|
|||
}
|
||||
|
||||
|
||||
private static class SearchParamExtractorR4BHostServices implements FHIRPathEngine.IEvaluationContext {
|
||||
private class SearchParamExtractorR4BHostServices implements FHIRPathEngine.IEvaluationContext {
|
||||
|
||||
private final Map<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
@Override
|
||||
public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -134,6 +134,10 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements
|
|||
|
||||
@Override
|
||||
public Base resolveReference(Object theAppContext, String theUrl, Base refContext) throws FHIRException {
|
||||
Base retVal = resolveResourceInBundleWithPlaceholderId(theAppContext, theUrl);
|
||||
if (retVal != null) {
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/*
|
||||
* When we're doing resolution within the SearchParamExtractor, if we want
|
||||
|
@ -143,7 +147,6 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements
|
|||
* Encounter.patient.where(resolve() is Patient)
|
||||
*/
|
||||
IdType url = new IdType(theUrl);
|
||||
Base retVal = null;
|
||||
if (isNotBlank(url.getResourceType())) {
|
||||
|
||||
retVal = myResourceTypeToStub.get(url.getResourceType());
|
||||
|
|
|
@ -26,9 +26,12 @@ import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
|||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.sl.cache.Cache;
|
||||
import ca.uhn.fhir.sl.cache.CacheFactory;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import ca.uhn.fhir.util.bundle.BundleEntryParts;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.exceptions.PathEngineException;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
import org.hl7.fhir.r5.context.IWorkerContext;
|
||||
import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.r5.model.Base;
|
||||
|
@ -88,18 +91,18 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements
|
|||
public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
|
||||
return () -> {
|
||||
ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path));
|
||||
return myFhirPathEngine.evaluate((Base) theResource, parsed);
|
||||
return myFhirPathEngine.evaluate(theResource, (Base) theResource, (Base) theResource, (Base) theResource, parsed);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static class SearchParamExtractorR5HostServices implements FHIRPathEngine.IEvaluationContext {
|
||||
private class SearchParamExtractorR5HostServices implements FHIRPathEngine.IEvaluationContext {
|
||||
|
||||
private final Map<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
@Override
|
||||
public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -129,6 +132,10 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements
|
|||
|
||||
@Override
|
||||
public Base resolveReference(Object appContext, String theUrl, Base refContext) throws FHIRException {
|
||||
Base retVal = resolveResourceInBundleWithPlaceholderId(appContext, theUrl);
|
||||
if (retVal != null) {
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/*
|
||||
* When we're doing resolution within the SearchParamExtractor, if we want
|
||||
|
@ -138,7 +145,6 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements
|
|||
* Encounter.patient.where(resolve() is Patient)
|
||||
*/
|
||||
IdType url = new IdType(theUrl);
|
||||
Base retVal = null;
|
||||
if (isNotBlank(url.getResourceType())) {
|
||||
|
||||
retVal = myResourceTypeToStub.get(url.getResourceType());
|
||||
|
@ -185,5 +191,4 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements
|
|||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ import ca.uhn.fhir.jpa.model.dao.JpaPid;
|
|||
import ca.uhn.fhir.jpa.model.entity.BasePartitionable;
|
||||
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.entity.IResourceIndexComboSearchParameter;
|
||||
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique;
|
||||
|
@ -49,6 +48,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
|
|||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
||||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
|
@ -67,6 +67,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
|
@ -117,12 +118,22 @@ public class SearchParamExtractorService {
|
|||
|
||||
// All search parameter types except Reference
|
||||
ResourceIndexedSearchParams normalParams = new ResourceIndexedSearchParams();
|
||||
extractSearchIndexParameters(theRequestDetails, normalParams, theResource);
|
||||
extractSearchIndexParameters(theRequestDetails, normalParams, theResource, ISearchParamExtractor.ALL_PARAMS);
|
||||
mergeParams(normalParams, theNewParams);
|
||||
|
||||
if (myStorageSettings.isIndexOnContainedResources()) {
|
||||
boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources();
|
||||
ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences = mySearchParamExtractor.extractResourceLinks(theResource, indexOnContainedResources);
|
||||
SearchParamExtractorService.handleWarnings(theRequestDetails, myInterceptorBroadcaster, indexedReferences);
|
||||
|
||||
if (indexOnContainedResources) {
|
||||
ResourceIndexedSearchParams containedParams = new ResourceIndexedSearchParams();
|
||||
extractSearchIndexParametersForContainedResources(theRequestDetails, containedParams, theResource, theEntity);
|
||||
extractSearchIndexParametersForContainedResources(theRequestDetails, containedParams, theResource, theEntity, indexedReferences);
|
||||
mergeParams(containedParams, theNewParams);
|
||||
}
|
||||
|
||||
if (myStorageSettings.isIndexOnUpliftedRefchains()) {
|
||||
ResourceIndexedSearchParams containedParams = new ResourceIndexedSearchParams();
|
||||
extractSearchIndexParametersForUpliftedRefchains(theRequestDetails, containedParams, theEntity, theRequestPartitionId, theTransactionDetails, indexedReferences);
|
||||
mergeParams(containedParams, theNewParams);
|
||||
}
|
||||
|
||||
|
@ -130,9 +141,9 @@ public class SearchParamExtractorService {
|
|||
populateResourceTables(theNewParams, theEntity);
|
||||
|
||||
// Reference search parameters
|
||||
extractResourceLinks(theRequestPartitionId, theExistingParams, theNewParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails);
|
||||
extractResourceLinks(theRequestPartitionId, theExistingParams, theNewParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails, indexedReferences);
|
||||
|
||||
if (myStorageSettings.isIndexOnContainedResources()) {
|
||||
if (indexOnContainedResources) {
|
||||
extractResourceLinksForContainedResources(theRequestPartitionId, theNewParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails);
|
||||
}
|
||||
|
||||
|
@ -144,57 +155,156 @@ public class SearchParamExtractorService {
|
|||
myStorageSettings = theStorageSettings;
|
||||
}
|
||||
|
||||
private void extractSearchIndexParametersForContainedResources(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity) {
|
||||
/**
|
||||
* Extract search parameter indexes for contained resources. E.g. if we
|
||||
* are storing a Patient with a contained Organization, we might extract
|
||||
* a String index on the Patient with paramName="organization.name" and
|
||||
* value="Org Name"
|
||||
*/
|
||||
private void extractSearchIndexParametersForContainedResources(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity, ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) {
|
||||
|
||||
FhirTerser terser = myContext.newTerser();
|
||||
|
||||
// 1. get all contained resources
|
||||
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
|
||||
ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true);
|
||||
|
||||
String spnamePrefix;
|
||||
ResourceIndexedSearchParams currParams;
|
||||
// 3. for each referenced search parameter, create an index
|
||||
for (PathAndRef nextPathAndRef : referencedSearchParamSet) {
|
||||
for (PathAndRef nextPathAndRef : theIndexedReferences) {
|
||||
|
||||
// 3.1 get the search parameter name as spname prefix
|
||||
spnamePrefix = nextPathAndRef.getSearchParamName();
|
||||
|
||||
if (spnamePrefix == null || nextPathAndRef.getRef() == null)
|
||||
if (spnamePrefix == null || (nextPathAndRef.getRef() == null && nextPathAndRef.getResource() == null))
|
||||
continue;
|
||||
|
||||
// 3.2 find the contained resource
|
||||
IBaseResource containedResource = findContainedResource(theContainedResources, nextPathAndRef.getRef());
|
||||
if (containedResource == null)
|
||||
continue;
|
||||
|
||||
// 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite loops
|
||||
if (theAlreadySeenResources.contains(containedResource)) {
|
||||
// 3.1.2 check if this ref actually applies here
|
||||
Set<String> searchParamsToIndex = theTargetIndexingStrategy.getChainedSearchParametersToIndexForPath(nextPathAndRef);
|
||||
if (searchParamsToIndex.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
currParams = new ResourceIndexedSearchParams();
|
||||
// 3.2 find the target resource
|
||||
IBaseResource targetResource = theTargetIndexingStrategy.fetchResourceAtPath(nextPathAndRef);
|
||||
if (targetResource == null)
|
||||
continue;
|
||||
|
||||
// 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite loops
|
||||
if (theAlreadySeenResources.contains(targetResource)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ResourceIndexedSearchParams currParams = new ResourceIndexedSearchParams();
|
||||
|
||||
// 3.3 create indexes for the current contained resource
|
||||
extractSearchIndexParameters(theRequestDetails, currParams, containedResource);
|
||||
extractSearchIndexParameters(theRequestDetails, currParams, targetResource, searchParamsToIndex);
|
||||
|
||||
// 3.4 recurse to process any other contained resources referenced by this one
|
||||
if (myStorageSettings.isIndexOnContainedResourcesRecursively()) {
|
||||
// Recursing is currently only allowed for contained resources and not
|
||||
// uplifted refchains because the latter could potentially kill performance
|
||||
// with the number of resource resolutions needed in order to handle
|
||||
// a single write. Maybe in the future we could add caching to improve
|
||||
// this
|
||||
if (theRecurse) {
|
||||
HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources);
|
||||
nextAlreadySeenResources.add(containedResource);
|
||||
extractSearchIndexParametersForContainedResources(theRequestDetails, currParams, containedResource, theEntity, theContainedResources, nextAlreadySeenResources);
|
||||
nextAlreadySeenResources.add(targetResource);
|
||||
|
||||
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
|
||||
// e.g. for Observation.subject contained reference
|
||||
// the SP_NAME = subject.family
|
||||
currParams.updateSpnamePrefixForIndexedOnContainedResource(theEntity.getResourceType(), spnamePrefix);
|
||||
currParams.updateSpnamePrefixForIndexOnUpliftedChain(theEntity.getResourceType(), nextPathAndRef.getSearchParamName());
|
||||
|
||||
// 3.6 merge to the mainParams
|
||||
// NOTE: the spname prefix is different
|
||||
|
@ -223,42 +333,42 @@ public class SearchParamExtractorService {
|
|||
theTargetParams.myCompositeParams.addAll(theSrcParams.myCompositeParams);
|
||||
}
|
||||
|
||||
void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource) {
|
||||
void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
|
||||
// Strings
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> strings = extractSearchParamStrings(theResource);
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> strings = extractSearchParamStrings(theResource, theParamsToIndex);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, strings);
|
||||
theParams.myStringParams.addAll(strings);
|
||||
|
||||
// Numbers
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> numbers = extractSearchParamNumber(theResource);
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> numbers = extractSearchParamNumber(theResource, theParamsToIndex);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, numbers);
|
||||
theParams.myNumberParams.addAll(numbers);
|
||||
|
||||
// Quantities
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> quantities = extractSearchParamQuantity(theResource);
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> quantities = extractSearchParamQuantity(theResource, theParamsToIndex);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantities);
|
||||
theParams.myQuantityParams.addAll(quantities);
|
||||
|
||||
if (myStorageSettings.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_STORAGE_SUPPORTED) || myStorageSettings.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED)) {
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> quantitiesNormalized = extractSearchParamQuantityNormalized(theResource);
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> quantitiesNormalized = extractSearchParamQuantityNormalized(theResource, theParamsToIndex);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantitiesNormalized);
|
||||
theParams.myQuantityNormalizedParams.addAll(quantitiesNormalized);
|
||||
}
|
||||
|
||||
// Dates
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> dates = extractSearchParamDates(theResource);
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> dates = extractSearchParamDates(theResource, theParamsToIndex);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, dates);
|
||||
theParams.myDateParams.addAll(dates);
|
||||
|
||||
// URIs
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> uris = extractSearchParamUri(theResource);
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> uris = extractSearchParamUri(theResource, theParamsToIndex);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, uris);
|
||||
theParams.myUriParams.addAll(uris);
|
||||
|
||||
// Tokens (can result in both Token and String, as we index the display name for
|
||||
// the types: Coding, CodeableConcept)
|
||||
ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> tokens = extractSearchParamTokens(theResource);
|
||||
ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> tokens = extractSearchParamTokens(theResource, theParamsToIndex);
|
||||
for (BaseResourceIndexedSearchParam next : tokens) {
|
||||
if (next instanceof ResourceIndexedSearchParamToken) {
|
||||
theParams.myTokenParams.add((ResourceIndexedSearchParamToken) next);
|
||||
|
@ -272,13 +382,13 @@ public class SearchParamExtractorService {
|
|||
// Composites
|
||||
// dst2 composites use stuff like value[x] , and we don't support them.
|
||||
if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> composites = extractSearchParamComposites(theResource);
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> composites = extractSearchParamComposites(theResource, theParamsToIndex);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, composites);
|
||||
theParams.myCompositeParams.addAll(composites);
|
||||
}
|
||||
|
||||
// Specials
|
||||
ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> specials = extractSearchParamSpecial(theResource);
|
||||
ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> specials = extractSearchParamSpecial(theResource, theParamsToIndex);
|
||||
for (BaseResourceIndexedSearchParam next : specials) {
|
||||
if (next instanceof ResourceIndexedSearchParamCoords) {
|
||||
theParams.myCoordsParams.add((ResourceIndexedSearchParamCoords) next);
|
||||
|
@ -304,20 +414,23 @@ public class SearchParamExtractorService {
|
|||
myContext = theContext;
|
||||
}
|
||||
|
||||
private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest) {
|
||||
extractResourceLinks(theRequestPartitionId, new ResourceIndexedSearchParams(), theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequest);
|
||||
private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest, ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) {
|
||||
extractResourceLinks(theRequestPartitionId, new ResourceIndexedSearchParams(), theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequest, theIndexedReferences);
|
||||
}
|
||||
|
||||
private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theExistingParams, ResourceIndexedSearchParams theNewParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest) {
|
||||
private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theExistingParams, ResourceIndexedSearchParams theNewParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest, ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) {
|
||||
String sourceResourceName = myContext.getResourceType(theResource);
|
||||
|
||||
ISearchParamExtractor.SearchParamSet<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource, false);
|
||||
SearchParamExtractorService.handleWarnings(theRequest, myInterceptorBroadcaster, refs);
|
||||
for (PathAndRef nextPathAndRef : theIndexedReferences) {
|
||||
if (nextPathAndRef.getRef() != null) {
|
||||
if (nextPathAndRef.getRef().getReferenceElement().isLocal()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (PathAndRef nextPathAndRef : refs) {
|
||||
RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(sourceResourceName, nextPathAndRef.getSearchParamName());
|
||||
extractResourceLinks(theRequestPartitionId, theExistingParams, theNewParams, theEntity, theTransactionDetails, sourceResourceName, searchParam, nextPathAndRef, theFailOnInvalidReference, theRequest);
|
||||
}
|
||||
}
|
||||
|
||||
theEntity.setHasLinks(theNewParams.myLinks.size() > 0);
|
||||
}
|
||||
|
@ -531,7 +644,8 @@ public class SearchParamExtractorService {
|
|||
currParams = new ResourceIndexedSearchParams();
|
||||
|
||||
// 3.3 create indexes for the current contained resource
|
||||
extractResourceLinks(theRequestPartitionId, currParams, theEntity, containedResource, theTransactionDetails, theFailOnInvalidReference, theRequest);
|
||||
ISearchParamExtractor.SearchParamSet<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
|
||||
if (myStorageSettings.isIndexOnContainedResourcesRecursively()) {
|
||||
|
@ -602,6 +716,14 @@ public class SearchParamExtractorService {
|
|||
return ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime, targetVersion);
|
||||
}
|
||||
|
||||
private RequestPartitionId determineResolverPartitionId(@Nonnull RequestPartitionId theRequestPartitionId) {
|
||||
RequestPartitionId targetRequestPartitionId = theRequestPartitionId;
|
||||
if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.getAllowReferencesAcrossPartitions() == PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED) {
|
||||
targetRequestPartitionId = RequestPartitionId.allPartitions();
|
||||
}
|
||||
return targetRequestPartitionId;
|
||||
}
|
||||
|
||||
private void populateResourceTable(Collection<? extends BaseResourceIndexedSearchParam> theParams, ResourceTable theResourceTable) {
|
||||
for (BaseResourceIndexedSearchParam next : theParams) {
|
||||
if (next.getResourcePid() == null) {
|
||||
|
@ -621,43 +743,42 @@ public class SearchParamExtractorService {
|
|||
}
|
||||
}
|
||||
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource) {
|
||||
return mySearchParamExtractor.extractSearchParamDates(theResource);
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
return mySearchParamExtractor.extractSearchParamDates(theResource, theParamsToIndex);
|
||||
}
|
||||
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource) {
|
||||
return mySearchParamExtractor.extractSearchParamNumber(theResource);
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
return mySearchParamExtractor.extractSearchParamNumber(theResource, theParamsToIndex);
|
||||
}
|
||||
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource) {
|
||||
return mySearchParamExtractor.extractSearchParamQuantity(theResource);
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
return mySearchParamExtractor.extractSearchParamQuantity(theResource, theParamsToIndex);
|
||||
}
|
||||
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource) {
|
||||
return mySearchParamExtractor.extractSearchParamQuantityNormalized(theResource);
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
return mySearchParamExtractor.extractSearchParamQuantityNormalized(theResource, theParamsToIndex);
|
||||
}
|
||||
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource) {
|
||||
return mySearchParamExtractor.extractSearchParamStrings(theResource);
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
return mySearchParamExtractor.extractSearchParamStrings(theResource, theParamsToIndex);
|
||||
}
|
||||
|
||||
private ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource) {
|
||||
return mySearchParamExtractor.extractSearchParamTokens(theResource);
|
||||
private ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
return mySearchParamExtractor.extractSearchParamTokens(theResource, theParamsToIndex);
|
||||
}
|
||||
|
||||
private ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource) {
|
||||
return mySearchParamExtractor.extractSearchParamSpecial(theResource);
|
||||
private ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
return mySearchParamExtractor.extractSearchParamSpecial(theResource, theParamsToIndex);
|
||||
}
|
||||
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource) {
|
||||
return mySearchParamExtractor.extractSearchParamUri(theResource);
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
return mySearchParamExtractor.extractSearchParamUri(theResource, theParamsToIndex);
|
||||
}
|
||||
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource) {
|
||||
return mySearchParamExtractor.extractSearchParamComposites(theResource);
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource, Set<String> theParamsToIndex) {
|
||||
return mySearchParamExtractor.extractSearchParamComposites(theResource, theParamsToIndex);
|
||||
}
|
||||
|
||||
|
||||
@VisibleForTesting
|
||||
void setInterceptorBroadcasterForUnitTest(IInterceptorBroadcaster theInterceptorBroadcaster) {
|
||||
myInterceptorBroadcaster = theInterceptorBroadcaster;
|
||||
|
@ -682,6 +803,32 @@ public class SearchParamExtractorService {
|
|||
populateResourceTableForComboParams(theParams.myComboTokenNonUnique, theEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface is used by {@link #extractSearchIndexParametersForTargetResources(RequestDetails, ResourceIndexedSearchParams, ResourceTable, Collection, IChainedSearchParameterExtractionStrategy, ISearchParamExtractor.SearchParamSet, boolean, boolean)}
|
||||
* in order to use that method for extracting chained search parameter indexes both
|
||||
* from contained resources and from uplifted refchains.
|
||||
*/
|
||||
private interface IChainedSearchParameterExtractionStrategy {
|
||||
|
||||
/**
|
||||
* Which search parameters should be indexed for the resource target
|
||||
* at the given path. In other words if thePathAndRef contains
|
||||
* "Patient/123", then we could return a Set containing "name" and "gender"
|
||||
* if we only want those two parameters to be indexed for the
|
||||
* resolved Patient resource with that ID.
|
||||
*/
|
||||
@Nonnull
|
||||
Set<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) {
|
||||
if (theSearchParamSet.getWarnings().isEmpty()) {
|
||||
return;
|
||||
|
|
|
@ -29,6 +29,7 @@ import ca.uhn.fhir.model.api.ExtensionDt;
|
|||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.util.DatatypeUtil;
|
||||
import ca.uhn.fhir.util.ExtensionUtil;
|
||||
import ca.uhn.fhir.util.FhirTerser;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import ca.uhn.fhir.util.PhoneticEncoderUtil;
|
||||
|
@ -362,19 +363,30 @@ public class SearchParameterCanonicalizer {
|
|||
*/
|
||||
protected void extractExtensions(IBaseResource theSearchParamResource, RuntimeSearchParam theRuntimeSearchParam) {
|
||||
if (theSearchParamResource instanceof IBaseHasExtensions) {
|
||||
List<? extends IBaseExtension<?, ?>> extensions = ((IBaseHasExtensions) theSearchParamResource).getExtension();
|
||||
for (IBaseExtension<?, ?> next : extensions) {
|
||||
List<? extends IBaseExtension<? extends IBaseExtension, ?>> extensions = (List<? extends IBaseExtension<? extends IBaseExtension, ?>>) ((IBaseHasExtensions) theSearchParamResource).getExtension();
|
||||
for (IBaseExtension<? extends IBaseExtension, ?> next : extensions) {
|
||||
String nextUrl = next.getUrl();
|
||||
if (isNotBlank(nextUrl)) {
|
||||
theRuntimeSearchParam.addExtension(nextUrl, next);
|
||||
if (HapiExtensions.EXT_SEARCHPARAM_PHONETIC_ENCODER.equals(nextUrl)) {
|
||||
setEncoder(theRuntimeSearchParam, next.getValue());
|
||||
} else if (HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN.equals(nextUrl)) {
|
||||
addUpliftRefchain(theRuntimeSearchParam, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void addUpliftRefchain(RuntimeSearchParam theRuntimeSearchParam, IBaseExtension<? 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) {
|
||||
if (theValue instanceof IPrimitiveType) {
|
||||
String stringValue = ((IPrimitiveType<?>) theValue).getValueAsString();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -24,11 +24,14 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
|||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.SearchParameter;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.hl7.fhir.r4.model.CodeType;
|
||||
import org.hl7.fhir.r4.model.Extension;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -39,9 +42,11 @@ import org.springframework.context.annotation.Bean;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.testcontainers.shaded.com.google.common.collect.Sets;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
|
@ -338,6 +343,38 @@ public class SearchParamRegistryImplTest {
|
|||
assertEquals("FOO", value.getValueAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpliftRefchains() {
|
||||
SearchParameter sp = new SearchParameter();
|
||||
Extension upliftRefChain = sp.addExtension().setUrl(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN);
|
||||
upliftRefChain.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE, new CodeType("name1"));
|
||||
upliftRefChain.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_ELEMENT_NAME, new StringType("element1"));
|
||||
Extension upliftRefChain2 = sp.addExtension().setUrl(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN);
|
||||
upliftRefChain2.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE, new CodeType("name2"));
|
||||
sp.setCode("subject");
|
||||
sp.setName("subject");
|
||||
sp.setDescription("Modified Subject");
|
||||
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
sp.setType(Enumerations.SearchParamType.REFERENCE);
|
||||
sp.setExpression("Encounter.subject");
|
||||
sp.addBase("Encounter");
|
||||
sp.addTarget("Patient");
|
||||
|
||||
ArrayList<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) {
|
||||
// Add a new search parameter entity
|
||||
List<ResourceTable> newEntities = new ArrayList(ourEntities);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
|||
myStorageSettings.setAllowMultipleDelete(true);
|
||||
myStorageSettings.setSearchPreFetchThresholds(new JpaStorageSettings().getSearchPreFetchThresholds());
|
||||
myStorageSettings.setReuseCachedSearchResultsForMillis(null);
|
||||
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -264,6 +265,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
|||
}
|
||||
|
||||
String url = "/AuditEvent?patient.name=Smith";
|
||||
logAllStringIndexes();
|
||||
|
||||
// execute
|
||||
myCaptureQueriesListener.clear();
|
||||
|
@ -868,6 +870,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
|||
}
|
||||
|
||||
String url = "/Observation?subject.organization.name=HealthCo";
|
||||
logAllStringIndexes();
|
||||
|
||||
// execute
|
||||
myCaptureQueriesListener.clear();
|
||||
|
@ -1527,7 +1530,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
|
|||
countUnionStatementsInGeneratedQuery("/Observation?patient.organization.partof.name=Smith", 7);
|
||||
|
||||
// If a reference in the chain has multiple potential target resource types, the number of subselects increases
|
||||
countUnionStatementsInGeneratedQuery("/Observation?subject.name=Smith", 3);
|
||||
// Note: This previously had 3 unions but 2 of the selects within were duplicates of each other
|
||||
countUnionStatementsInGeneratedQuery("/Observation?subject.name=Smith", 2);
|
||||
|
||||
// If such a reference if qualified to restrict the type, the number goes back down
|
||||
countUnionStatementsInGeneratedQuery("/Observation?subject:Location.name=Smith", 1);
|
||||
|
|
|
@ -83,6 +83,7 @@ import org.hl7.fhir.r4.model.CodeableConcept;
|
|||
import org.hl7.fhir.r4.model.Coding;
|
||||
import org.hl7.fhir.r4.model.Communication;
|
||||
import org.hl7.fhir.r4.model.CommunicationRequest;
|
||||
import org.hl7.fhir.r4.model.Composition;
|
||||
import org.hl7.fhir.r4.model.Condition;
|
||||
import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem;
|
||||
import org.hl7.fhir.r4.model.DateTimeType;
|
||||
|
@ -137,6 +138,8 @@ import org.junit.jupiter.api.DisplayName;
|
|||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -5733,6 +5736,79 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Index for
|
||||
* [base]/Bundle?composition.patient.identifier=foo
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource({"urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b", "Patient/ABC"})
|
||||
public void testCreateAndSearchForFullyChainedSearchParameter(String thePatientId) {
|
||||
// Setup 1
|
||||
|
||||
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
|
||||
|
||||
SearchParameter sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/Bundle-composition-patient-identifier");
|
||||
sp.setCode("composition.patient.identifier");
|
||||
sp.setName("composition.patient.identifier");
|
||||
sp.setUrl("http://example.org/SearchParameter/Bundle-composition-patient-identifier");
|
||||
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
sp.setType(Enumerations.SearchParamType.TOKEN);
|
||||
sp.setExpression("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier");
|
||||
sp.addBase("Bundle");
|
||||
ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp));
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
|
||||
// Test 1
|
||||
|
||||
Composition composition = new Composition();
|
||||
composition.setSubject(new Reference(thePatientId));
|
||||
|
||||
Patient patient = new Patient();
|
||||
patient.setId(new IdType(thePatientId));
|
||||
patient.addIdentifier().setSystem("http://foo").setValue("bar");
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.setType(Bundle.BundleType.DOCUMENT);
|
||||
bundle.addEntry().setResource(composition);
|
||||
bundle.addEntry().setResource(patient);
|
||||
|
||||
myBundleDao.create(bundle, mySrd);
|
||||
|
||||
Bundle bundle2 = new Bundle();
|
||||
bundle2.setType(Bundle.BundleType.DOCUMENT);
|
||||
myBundleDao.create(bundle2, mySrd);
|
||||
|
||||
// Verify 1
|
||||
runInTransaction(() -> {
|
||||
logAllTokenIndexes();
|
||||
|
||||
List<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
|
||||
public class TagBelowTests {
|
||||
|
|
|
@ -13,6 +13,7 @@ import ca.uhn.fhir.rest.api.CacheControlDirective;
|
|||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.system.HapiSystemProperties;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.hapi.rest.server.helper.BatchHelperR4;
|
||||
|
@ -53,6 +54,8 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv
|
|||
|
||||
myReindexParameterCache = myStorageSettings.isMarkResourcesForReindexingUponSearchParameterChange();
|
||||
myStorageSettings.setMarkResourcesForReindexingUponSearchParameterChange(false);
|
||||
|
||||
HapiSystemProperties.enableUnitTestMode();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
@ -177,7 +180,7 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv
|
|||
List<String> alleleObservationIds = reindexTestHelper.getAlleleObservationIds(myClient);
|
||||
// Only the one in the first tenant should be indexed
|
||||
myTenantClientInterceptor.setTenantId(TENANT_A);
|
||||
await().until(() -> reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1));
|
||||
assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1));
|
||||
assertEquals(obsFinalA.getIdPart(), alleleObservationIds.get(0));
|
||||
myTenantClientInterceptor.setTenantId(TENANT_B);
|
||||
MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(0));
|
||||
|
@ -210,7 +213,7 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv
|
|||
});
|
||||
|
||||
myTenantClientInterceptor.setTenantId(DEFAULT_PARTITION_NAME);
|
||||
await().until(() -> reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1));
|
||||
assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -258,7 +261,7 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv
|
|||
List<String> alleleObservationIds = reindexTestHelper.getAlleleObservationIds(myClient);
|
||||
// Only the one in the first tenant should be indexed
|
||||
myTenantClientInterceptor.setTenantId(TENANT_A);
|
||||
MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1));
|
||||
assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1));
|
||||
assertEquals(obsFinalA.getIdPart(), alleleObservationIds.get(0));
|
||||
myTenantClientInterceptor.setTenantId(TENANT_B);
|
||||
MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(0));
|
||||
|
|
|
@ -59,6 +59,8 @@ import org.apache.commons.collections4.CollectionUtils;
|
|||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
|
@ -8166,6 +8168,15 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
|
|||
myIsMissing = theIsMissing;
|
||||
myIsValuePresentOnResource = theHasField;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this, ToStringStyle.NO_CLASS_NAME_STYLE)
|
||||
.append("valuePresent", myIsValuePresentOnResource)
|
||||
.append("isMissing", myIsMissing)
|
||||
.append("enableMissingFields", myEnableMissingFieldsValue)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ca.uhn.fhir.jpa.dao.r5;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.test.config.TestHSearchAddInConfig;
|
||||
|
@ -11,16 +12,24 @@ import ca.uhn.fhir.rest.param.ReferenceParam;
|
|||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.r5.model.Bundle;
|
||||
import org.hl7.fhir.r5.model.ClinicalUseDefinition;
|
||||
import org.hl7.fhir.r5.model.CodeableConcept;
|
||||
import org.hl7.fhir.r5.model.Coding;
|
||||
import org.hl7.fhir.r5.model.Composition;
|
||||
import org.hl7.fhir.r5.model.Enumerations;
|
||||
import org.hl7.fhir.r5.model.IdType;
|
||||
import org.hl7.fhir.r5.model.ObservationDefinition;
|
||||
import org.hl7.fhir.r5.model.Organization;
|
||||
import org.hl7.fhir.r5.model.Patient;
|
||||
import org.hl7.fhir.r5.model.Practitioner;
|
||||
import org.hl7.fhir.r5.model.PractitionerRole;
|
||||
import org.hl7.fhir.r5.model.Reference;
|
||||
import org.hl7.fhir.r5.model.SearchParameter;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
import java.util.Date;
|
||||
|
@ -28,6 +37,7 @@ import java.util.List;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@ContextConfiguration(classes = TestHSearchAddInConfig.NoFT.class)
|
||||
|
@ -35,6 +45,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||
public class FhirResourceDaoR5SearchNoFtTest extends BaseJpaR5Test {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR5SearchNoFtTest.class);
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasWithTargetReference() {
|
||||
Organization org = new Organization();
|
||||
|
@ -221,4 +236,79 @@ public class FhirResourceDaoR5SearchNoFtTest extends BaseJpaR5Test {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Index for
|
||||
* [base]/Bundle?composition.patient.identifier=foo
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource({"urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b", "Patient/ABC"})
|
||||
public void testCreateAndSearchForFullyChainedSearchParameter(String thePatientId) {
|
||||
// Setup 1
|
||||
|
||||
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
|
||||
|
||||
SearchParameter sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/Bundle-composition-patient-identifier");
|
||||
sp.setCode("composition.patient.identifier");
|
||||
sp.setName("composition.patient.identifier");
|
||||
sp.setUrl("http://example.org/SearchParameter/Bundle-composition-patient-identifier");
|
||||
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
sp.setType(Enumerations.SearchParamType.TOKEN);
|
||||
sp.setExpression("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier");
|
||||
sp.addBase("Bundle");
|
||||
ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp));
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
|
||||
// Test 1
|
||||
|
||||
Composition composition = new Composition();
|
||||
composition.addSubject().setReference(thePatientId);
|
||||
|
||||
Patient patient = new Patient();
|
||||
patient.setId(new IdType(thePatientId));
|
||||
patient.addIdentifier().setSystem("http://foo").setValue("bar");
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.setType(Bundle.BundleType.DOCUMENT);
|
||||
bundle.addEntry().setResource(composition);
|
||||
bundle.addEntry().setResource(patient);
|
||||
|
||||
myBundleDao.create(bundle, mySrd);
|
||||
|
||||
Bundle bundle2 = new Bundle();
|
||||
bundle2.setType(Bundle.BundleType.DOCUMENT);
|
||||
myBundleDao.create(bundle2, mySrd);
|
||||
|
||||
// Verify 1
|
||||
runInTransaction(() -> {
|
||||
logAllTokenIndexes();
|
||||
|
||||
List<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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package ca.uhn.fhirtest.config;
|
|||
|
||||
import ca.uhn.fhir.batch2.jobs.config.Batch2JobsConfig;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
||||
import ca.uhn.fhir.jpa.api.config.ThreadPoolFactoryConfig;
|
||||
import ca.uhn.fhir.jpa.batch2.JpaBatch2Config;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
|
@ -105,4 +106,9 @@ public class CommonConfig {
|
|||
return new ScheduledSubscriptionDeleter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommonJpaStorageSettingsConfigurer commonJpaStorageSettingsConfigurer(JpaStorageSettings theStorageSettings) {
|
||||
return new CommonJpaStorageSettingsConfigurer(theStorageSettings);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
|||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
@ -60,6 +61,7 @@ public class TransactionDetails {
|
|||
private List<Runnable> myRollbackUndoActions = Collections.emptyList();
|
||||
private Map<String, IResourcePersistentId> myResolvedResourceIds = Collections.emptyMap();
|
||||
private Map<String, IResourcePersistentId> myResolvedMatchUrls = Collections.emptyMap();
|
||||
private Map<String, Supplier<IBaseResource>> myResolvedResources = Collections.emptyMap();
|
||||
private Map<String, Object> myUserData;
|
||||
private ListMultimap<Pointcut, HookParams> myDeferredInterceptorBroadcasts;
|
||||
private EnumSet<Pointcut> myDeferredInterceptorBroadcastPointcuts;
|
||||
|
@ -123,6 +125,21 @@ public class TransactionDetails {
|
|||
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
|
||||
*/
|
||||
|
@ -150,6 +167,30 @@ public class TransactionDetails {
|
|||
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() {
|
||||
return myResolvedMatchUrls;
|
||||
}
|
||||
|
@ -315,5 +356,6 @@ public class TransactionDetails {
|
|||
public boolean isFhirTransaction() {
|
||||
return myFhirTransaction;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<artifactId>hapi-fhir-serviceloaders</artifactId>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<artifactId>hapi-fhir-serviceloaders</artifactId>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-caching-api</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<artifactId>hapi-fhir-serviceloaders</artifactId>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<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>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.hapi.fhir.batch2.test;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server - Batch2 specification tests
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.api.IJobPersistence;
|
||||
import ca.uhn.fhir.batch2.coordinator.BatchWorkChunk;
|
||||
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.5.6-SNAPSHOT</version>
|
||||
<version>6.5.7-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
package ca.uhn.fhir.batch2.api;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server - Batch2 Task Processor
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.batch2.coordinator.BatchWorkChunk;
|
||||
import ca.uhn.fhir.batch2.model.WorkChunk;
|
||||
import ca.uhn.fhir.batch2.model.WorkChunkCompletionEvent;
|
||||
|
|
|
@ -32,6 +32,7 @@ import ca.uhn.fhir.batch2.jobs.parameters.PartitionedJobParameters;
|
|||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.pid.IResourcePidList;
|
||||
import ca.uhn.fhir.jpa.api.pid.TypedResourcePid;
|
||||
import ca.uhn.fhir.system.HapiSystemProperties;
|
||||
import ca.uhn.fhir.util.Logs;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
|
@ -85,7 +86,7 @@ public class ResourceIdListStep<PT extends PartitionedJobParameters, IT extends
|
|||
}
|
||||
|
||||
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
|
||||
// which is failing intermittently. If that stops, makes sense to remove this
|
||||
ourLog.info(" * PIDS: {}", nextChunk);
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
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.HashCodeBuilder;
|
||||
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
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.HashCodeBuilder;
|
||||
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
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.EnumSet;
|
||||
import java.util.Set;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue