From 7248b4e18e290b519d35788f0b6594cc80866fbc Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Mon, 20 Apr 2020 11:02:27 -0400 Subject: [PATCH] Finish docs --- .../ca/uhn/fhir/interceptor/api/Hook.java | 1 + .../ca/uhn/fhir/i18n/hapi-messages.properties | 2 +- .../uhn/hapi/fhir/docs/PartitionExamples.java | 133 ++++++++ .../docs/images/jpa_erd_resource_links.svg | 2 +- .../fhir/docs/images/jpa_erd_resources.svg | 2 +- .../docs/images/jpa_erd_search_indexes.svg | 1 + .../built_in_server_interceptors.md | 14 +- .../hapi/fhir/docs/server_jpa/partitioning.md | 54 ++- .../uhn/hapi/fhir/docs/server_jpa/schema.md | 116 ++++++- .../ca/uhn/fhir/jpa/config/BaseConfig.java | 4 +- .../fhir/jpa/dao/FulltextSearchSvcImpl.java | 11 +- .../ca/uhn/fhir/jpa/dao/SearchBuilder.java | 6 +- .../dao/index/DaoResourceLinkResolver.java | 10 +- .../fhir/jpa/dao/index/IdHelperService.java | 41 +-- .../dao/predicate/BasePredicateBuilder.java | 20 +- .../dao/predicate/PredicateBuilderDate.java | 26 +- .../predicate/PredicateBuilderReference.java | 28 +- .../dao/predicate/PredicateBuilderTag.java | 7 +- .../java/ca/uhn/fhir/jpa/util/TestUtil.java | 4 - .../r4/FhirResourceDaoR4SearchNoFtTest.java | 46 +++ .../fhir/jpa/dao/r4/PartitioningR4Test.java | 311 +++++++++++++++++- .../provider/r4/MultitenantServerR4Test.java | 2 +- .../jpa/provider/r4/SystemProviderR4Test.java | 8 +- .../ResourceIndexedSearchParamDate.java | 2 +- .../RequestTenantPartitionInterceptor.java | 21 +- .../UrlBaseTenantIdentificationStrategy.java | 11 +- 26 files changed, 772 insertions(+), 111 deletions(-) create mode 100644 hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/PartitionExamples.java create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_search_indexes.svg rename {hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa => hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor}/partition/RequestTenantPartitionInterceptor.java (68%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Hook.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Hook.java index 8a77920728e..41c8a4c63f8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Hook.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Hook.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.interceptor.api; */ import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 6f60827bafa..ff001c3e1fc 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -157,4 +157,4 @@ ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantCreateDuplicatePartitionNam ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantDeleteDefaultPartition=Can not delete default partition ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantRenameDefaultPartition=Can not rename default partition -ca.uhn.fhir.jpa.partition.RequestTenantPartitionInterceptor.unknownTenantName=Unknown tenant: {0} +ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor.unknownTenantName=Unknown tenant: {0} diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/PartitionExamples.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/PartitionExamples.java new file mode 100644 index 00000000000..1c6cd103792 --- /dev/null +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/PartitionExamples.java @@ -0,0 +1,133 @@ +package ca.uhn.hapi.fhir.docs; + +/*- + * #%L + * HAPI FHIR - Docs + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * 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.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.interceptor.model.PartitionId; +import ca.uhn.fhir.jpa.model.config.PartitionConfig; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("InnerClassMayBeStatic") +public class PartitionExamples { + + public void multitenantServer() { + + } + + + // START SNIPPET: partitionInterceptorRequestPartition + @Interceptor + public class RequestTenantPartitionInterceptor { + + @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE) + public PartitionId PartitionIdentifyCreate(ServletRequestDetails theRequestDetails) { + return extractPartitionIdFromRequest(theRequestDetails); + } + + @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ) + public PartitionId PartitionIdentifyRead(ServletRequestDetails theRequestDetails) { + return extractPartitionIdFromRequest(theRequestDetails); + } + + private PartitionId extractPartitionIdFromRequest(ServletRequestDetails theRequestDetails) { + // We will use the tenant ID that came from the request as the partition name + String tenantId = theRequestDetails.getTenantId(); + return PartitionId.forPartitionName(tenantId); + } + + } + // END SNIPPET: partitionInterceptorRequestPartition + + + // START SNIPPET: partitionInterceptorHeaders + @Interceptor + public class CustomHeaderBasedPartitionInterceptor { + + @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE) + public PartitionId PartitionIdentifyCreate(ServletRequestDetails theRequestDetails) { + String partitionName = theRequestDetails.getHeader("X-Partition-Name"); + return PartitionId.forPartitionName(partitionName); + } + + @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ) + public PartitionId PartitionIdentifyRead(ServletRequestDetails theRequestDetails) { + String partitionName = theRequestDetails.getHeader("X-Partition-Name"); + return PartitionId.forPartitionName(partitionName); + } + + } + // END SNIPPET: partitionInterceptorHeaders + + + // START SNIPPET: partitionInterceptorResourceContents + @Interceptor + public class ResourceTypePartitionInterceptor { + + @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE) + public PartitionId PartitionIdentifyCreate(IBaseResource theResource) { + if (theResource instanceof Patient) { + return PartitionId.forPartitionName("PATIENT"); + } else if (theResource instanceof Observation) { + return PartitionId.forPartitionName("OBSERVATION"); + } else { + return PartitionId.forPartitionName("OTHER"); + } + } + + } + // END SNIPPET: partitionInterceptorResourceContents + + + // START SNIPPET: multitenantServer + public class MultitenantServer extends RestfulServer { + + @Autowired + private PartitionConfig myPartitionConfig; + + @Override + protected void initialize() { + + // Enable partitioning + myPartitionConfig.setPartitioningEnabled(true); + + // Set the tenant identification strategy + setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy()); + + // Use the tenant ID supplied by the tenant identification strategy + // to serve as the partitioning ID + registerInterceptor(new RequestTenantPartitionInterceptor()); + + // ....Register some providers and other things.... + + } + } + // END SNIPPET: multitenantServer + + +} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_resource_links.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_resource_links.svg index 35602100172..d845b607901 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_resource_links.svg +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_resource_links.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_resources.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_resources.svg index 759a50a4bfe..dd7973051cc 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_resources.svg +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_resources.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_search_indexes.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_search_indexes.svg new file mode 100644 index 00000000000..c2fcc548ac7 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/jpa_erd_search_indexes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md index 4cceda4e377..eaf3690558c 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md @@ -24,6 +24,16 @@ This interceptor will then produce output similar to the following: 2014-09-04 03:30:00.443 Source[127.0.0.1] Operation[search-type Organization] UA[Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)] Params[] ``` + + +# Partitioning: Multitenant Request Partition + +If the JPA server has [partitioning](/docs/server_jpa/partitioning.html) enabled, the RequestTenantPartitionInterceptor can be used in combination with a [Tenant Identification Strategy](/docs/server_plain/multitenancy.html) in order to achieve a multitenant solution. See [JPA Server Partitioning](/docs/server_jpa/partitioning.html) for more information on partitioning. + +* [RequestTenantPartitionInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.html) +* [RequestTenantPartitionInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.java) + + # Response Customizing: Syntax Highlighting The ResponseHighlighterInterceptor detects when a request is coming from a browser and returns HTML with syntax highlighted XML/JSON instead of just the raw text. In other words, if a user uses a browser to request `http://foo/Patient/1` by typing this address into their URL bar, they will get a nicely formatted HTML back with a human readable version of the content. This is particularly helpful for testers and public/development APIs where users are likely to invoke the API directly to see how it works. @@ -150,8 +160,8 @@ Some security audit tools require that servers return an HTTP 405 if an unsuppor When using Subscriptions, the debug log interceptor can be used to add a number of additional lines to the server logs showing the internals of the subscription processing pipeline. -* [SubscriptionDebugLogInterceptor JavaDoc](/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.html) -* [SubscriptionDebugLogInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master//hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.java) +* [SubscriptionDebugLogInterceptor JavaDoc](/apidocs/hapi-fhir-jpaserver-subscription/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.html) +* [SubscriptionDebugLogInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.java) # Request Pre-Processing: Override Meta.source diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md index 6ebe6849bfd..a925246ab33 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md @@ -21,13 +21,13 @@ Partitioning involves the use of two dedicated columns to many tables within the * **PARTITION_ID** – This is an integer indicating the specific partition that a given resource is placed in. This column can also be *NULL*, meaning that the given resource is in the **Default Partition**. * **PARTITION_DATE** – This is a date/time column that can be assigned an arbitrary value depending on your use case. Typically, this would be used for use cases where data should be automatically dropped after a certain time period using native database partition drops. -When partitioning is used, these two columns will be populated with the same value on all resource-specific tables (this includes [HFJ_RESOURCE](./schema.html#HFJ_RESOURCE) and all tables that have a foreign key relationship to it including [HFJ_RES_VER](./schema.html#HFJ_RES_VER), [HFJ_RESLINK](./schema.html#HFJ_RES_LINK), [HFJ_SPIDX_*](./schema.html#indexes), etc.) +When partitioning is used, these two columns will be populated with the same value on all resource-specific tables (this includes [HFJ_RESOURCE](./schema.html#HFJ_RESOURCE) and all tables that have a foreign key relationship to it including [HFJ_RES_VER](./schema.html#HFJ_RES_VER), [HFJ_RESLINK](./schema.html#HFJ_RES_LINK), [HFJ_SPIDX_*](./schema.html#search-indexes), etc.) -At the time that a resource is being **created**, an [interceptor hook](#partition-iInterceptors) is invoked in order to request the partition ID and date, and these will be written to the resource. +At the time that a resource is being **created**, an [interceptor hook](#partition-interceptors) is invoked in order to request the partition ID and date, and these will be written to the resource. At the time that a resource is being **updated**, the partition ID and date from the previous version will be used. -When a **read operation** is being performed (e.g. a read, search, history, etc.), a separate [interceptor hook](#partition-iInterceptors) is invoked in order to determine whether the operation should target a specific partition. The outcome of this hook determines how the partitioning manifests itself to the end user: +When a **read operation** is being performed (e.g. a read, search, history, etc.), a separate [interceptor hook](#partition-interceptors) is invoked in order to determine whether the operation should target a specific partition. The outcome of this hook determines how the partitioning manifests itself to the end user: * If all read operations are scoped by the interceptor to only apply to a single partition, then the partitioning behaves as a **multitenant** solution. * If read operations are scopes to all partitions, then the partitioning is simply partitioning the data into logical segments. @@ -40,7 +40,9 @@ The [PartitionConfig](/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/c The following settings can be enabled: -* Include Partition in Search Hashes: +* **Include Partition in Search Hashes** ([JavaDoc](/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionConfig.html#setIncludePartitionInSearchHashes(boolean))): If this feature is enabled, partition IDs will be factored into [Search Hashes](./schema.html#search-hashes). When this flag is not set (as is the default), when a search requests a specific partition, an additional SQL WHERE predicate is added to the query to explicitly request the given partition ID. When this flag is set, this additional WHERE predicate is not necessary since the partition is factored into the hash value being searched on. Setting this flag avoids the need to manually adjust indexes against the HFJ_SPIDX tables. Note that this flag should **not be used in environments where partitioning is being used for security purposes**, since it is possible for a user to reverse engineer false hash collisions. + +* **Cross-Partition Reference Mode**: ([JavaDoc](/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionConfig.html#setAllowReferencesAcrossPartitions(ca.uhn.fhir.jpa.model.config.PartitionConfig.CrossPartitionReferenceMode))): This setting controls whether resources in one partition should be allowed to create references to resources in other partitions. # Partition Interceptors @@ -57,14 +59,54 @@ The criteria for determining the partition will depend on your use case. For exa * If you are implementing multi-tenancy the partition might be determined by using the [Request Tenant ID](/docs/server_plain/multitenancy.html). It could also be determined by looking at request headers, or the authorized user/session context, etc. -* If you are implementing segmented data partitioning, the partition might be determined by examining the actual resource being created, by the identity of the sending system, etc. +* If you are implementing segmented data partitioning, the partition might be determined by examining the actpartitionInterceptorHeadersual resource being created, by the identity of the sending system, etc. ## Identify Partition for Read (Optional) A hook against the [`Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE`](/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PARTITION_IDENTIFY_CREATE) pointcut must be registered, and this hook method will be invoked every time a resource is being created in order to determine the partition to create the resource in. -## Example: Using Request Tenants +## Example: Partitioning based on Tenant ID +The [RequestTenantPartitionInterceptor](/docs/interceptors/built_in_server_interceptors.html#request-tenant-partition-interceptor) uses the request tenant ID to determine the partition name. A simplified version of its source is shown below: + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/PartitionExamples.java|partitionInterceptorHeaders}} +``` + +## Example: Partitioning based on headers + +If requests are coming from a trusted system, that system might be relied on to determine the partition for reads and writes. + +The following example shows a simple partition interceptor that determines the partition name by looking at a custom HTTP header: + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/PartitionExamples.java|partitionInterceptorHeaders}} +``` + +## Example: Using Resource Contents + +When creating resources, the contents of the resource can also be factored into the decision on which tenant to use. The following example shows a very simple algorithm, placing resources into one of three partitions based on the resource type. Other contents in the resource could also be used instead. + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/PartitionExamples.java|partitionInterceptorResourceContents}} +``` + + +# Complete Example: Using Request Tenants + +In order to achieve a multitenant configuration, the following configuration steps must be taken: + +* Partitioning must be enabled. +* A [Tenant Identification Strategy](/docs/server_plain/multitenancy.html) must be enabled on the RestfulServer. +* A [RequestTenantPartitionInterceptor](/docs/interceptors/built_in_server_interceptors.html#request-tenant-partition-interceptor) instance must be registered as an interceptor. + +Additionally, indexes will likely need to be tuned in order to support the partition-aware queries. + +The following snippet shows a server with this configuration. + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/PartitionExamples.java|multitenantServer}} +``` diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md index 7722e35d6b1..c65cfe27a87 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md @@ -71,7 +71,7 @@ The HFJ_RESOURCE table indicates a single resource of any type in the database. This column contains the FHIR version associated with this resource, using a constant drawn - from FhirVersionEnum. + from FhirVersionEnum. Not to be confused with RES_VER above. @@ -407,9 +407,119 @@ When a resource is created or updated, it is indexed for searching. Any search p - + # Background: Search Indexes -The HFJ_SPIDX (Search Parameter Index) tables are used to index resources for searching. When a resource is created or updated, a set of rows in these tables will be added. These are used for finding appropriate rows to return when performing FHIR searches. +The HFJ_SPIDX (Search Parameter Index) tables are used to index resources for searching. When a resource is created or updated, a set of rows in these tables will be added. These are used for finding appropriate rows to return when performing FHIR searches. There are dedicated tables for supporting each of the non-reference [FHIR Search Datatypes](http://hl7.org/fhir/search.html): Date, Number, Quantity, String, Token, and URI. Note that Reference search parameters are implemented using the [HFJ_RES_LINK](#HFJ_RES_LINK) table above. + + + +## Search Hashes + +The SPIDX tables leverage "hash columns", which contain a hash of multiple columns in order to reduce index size and improve search performance. Hashes currently use the [MurmurHash3_x64_128](https://en.wikipedia.org/wiki/MurmurHash) hash algorithm, keeping only the first 64 bits in order to produce a LongInt value. + +For example, all search index tables have columns for storing the search parameter name (**SP_NAME**) and resource type (**RES_TYPE**). An additional column which hashes these two values is provided, called **HASH_IDENTITY**. + +In some configurations, the partition ID is also factored into the hashes. + +## Tables + +Search Indexes + +## Columns + +The following columns are common to **all HFJ_SPIDX_xxx tables**. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRelationshipsDatatypeNullableDescription
PARTITION_IDIntegerNullable + This is the optional partition ID, if the resource is in a partition. See Partitioning. + Note that the partition indicated by the PARTITION_ID and PARTITION_DATE columns refers to the partition + of the SOURCE resource, and not necessarily the TARGET. +
PARTITION_DATETimestampNullable + This is the optional partition date, if the resource is in a partition. See Partitioning. + Note that the partition indicated by the PARTITION_ID and PARTITION_DATE columns refers to the partition + of the SOURCE resource, and not necessarily the TARGET. +
SP_IDLong + Holds the persistent ID +
RES_IDFK to HFJ_RESOURCEString + Contains the PID of the resource being indexed. +
SP_NAMEString + This is the name of the search parameter being indexed. +
RES_TYPEString + This is the name of the resource being indexed. +
SP_UPDATEDTimestamp + This is the time that this row was last updated. +
SP_MISSINGboolean + If this row represents a search parameter that is **not** populated at all in the resource being indexed, + this will be populated with the value `true`. Otherwise it will be populated with `false`. +
+ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 5c634ddb39b..ff084d1d0eb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -22,7 +22,7 @@ import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; -import ca.uhn.fhir.jpa.partition.RequestTenantPartitionInterceptor; +import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.sched.AutowiringSpringBeanJobFactory; @@ -42,8 +42,6 @@ import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl; import ca.uhn.fhir.jpa.searchparam.config.SearchParamConfig; import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; -import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices; import org.hibernate.jpa.HibernatePersistenceProvider; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java index 32890165607..c8b3cb310bf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.interceptor.model.PartitionId; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; +import ca.uhn.fhir.jpa.model.config.PartitionConfig; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperService; @@ -283,6 +284,9 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { @Autowired private IRequestPartitionHelperService myRequestPartitionHelperService; + @Autowired + private PartitionConfig myPartitionConfig; + @Transactional() @Override public List suggestKeywords(String theContext, String theSearchParam, String theText, RequestDetails theRequest) { @@ -297,8 +301,10 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { throw new InvalidRequestException("Invalid context: " + theContext); } - // FIXME: this method should require a resource type - PartitionId partitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, null); + // Partitioning is not supported for this operation + Validate.isTrue(myPartitionConfig.isPartitioningEnabled() == false, "Suggest keywords not supported for partitioned system"); + PartitionId partitionId = null; + ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(partitionId, contextParts[0], contextParts[1]); FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager); @@ -315,7 +321,6 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { .sentence(theText.toLowerCase()).createQuery(); Query query = qb.bool() -// .must(qb.keyword().onField("myResourceLinks.myTargetResourcePid").matching(pid).createQuery()) .must(qb.keyword().onField("myResourceLinksField").matching(pid.toString()).createQuery()) .must(textQuery) .createQuery(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index 8b98d9664e4..248d9a0b1d2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -360,7 +360,11 @@ public class SearchBuilder implements ISearchBuilder { } myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myDeleted"))); if (myPartitionId != null) { - myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myPartitionIdValue"), myPartitionId.getPartitionId())); + if (myPartitionId.getPartitionId() != null) { + myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myPartitionIdValue").as(Integer.class), myPartitionId.getPartitionId())); + } else { + myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myPartitionIdValue").as(Integer.class))); + } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java index 4001f931275..c305bb9930c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java @@ -64,11 +64,11 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver { private DaoRegistry myDaoRegistry; @Override - public IResourceLookup findTargetResource(PartitionId thePartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theTypeString, Class theType, IBaseReference theReference, RequestDetails theRequest) { + public IResourceLookup findTargetResource(PartitionId thePartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theResourceType, Class theType, IBaseReference theReference, RequestDetails theRequest) { IResourceLookup resolvedResource; String idPart = theSourceResourceId.getIdPart(); try { - resolvedResource = myIdHelperService.resolveResourceIdentity(thePartitionId, theTypeString, idPart, theRequest); + resolvedResource = myIdHelperService.resolveResourceIdentity(thePartitionId, theResourceType, idPart, theRequest); ourLog.trace("Translated {}/{} to resource PID {}", theType, idPart, resolvedResource); } catch (ResourceNotFoundException e) { @@ -89,8 +89,8 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver { } ourLog.trace("Resolved resource of type {} as PID: {}", resolvedResource.getResourceType(), resolvedResource.getResourceId()); - if (!theTypeString.equals(resolvedResource.getResourceType())) { - ourLog.error("Resource with PID {} was of type {} and wanted {}", resolvedResource.getResourceId(), theTypeString, resolvedResource.getResourceType()); + if (!theResourceType.equals(resolvedResource.getResourceType())) { + ourLog.error("Resource with PID {} was of type {} and wanted {}", resolvedResource.getResourceId(), theResourceType, resolvedResource.getResourceType()); throw new UnprocessableEntityException("Resource contains reference to unknown resource ID " + theSourceResourceId.getValue()); } @@ -99,7 +99,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver { throw new InvalidRequestException("Resource " + resName + "/" + idPart + " is deleted, specified in path: " + theSourcePath); } - if (!theSearchParam.hasTargets() && theSearchParam.getTargets().contains(theTypeString)) { + if (!theSearchParam.hasTargets() && theSearchParam.getTargets().contains(theResourceType)) { return null; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java index bebd350b647..d53cfbc2a6b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java @@ -21,9 +21,7 @@ package ca.uhn.fhir.jpa.dao.index; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; -import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.PartitionId; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; @@ -32,13 +30,10 @@ import ca.uhn.fhir.jpa.model.cross.IResourceLookup; import ca.uhn.fhir.jpa.model.cross.ResourceLookup; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.ForcedId; -import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; -import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.collect.ListMultimap; @@ -68,6 +63,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * This class is used to convert between PIDs (the internal primary key for a particular resource as @@ -122,9 +118,9 @@ public class IdHelperService { * @throws ResourceNotFoundException If the ID can not be found */ @Nonnull - public IResourceLookup resolveResourceIdentity(PartitionId thePartitionId, String theResourceName, String theResourceId, RequestDetails theRequestDetails) throws ResourceNotFoundException { + public IResourceLookup resolveResourceIdentity(PartitionId thePartitionId, String theResourceType, String theResourceId, RequestDetails theRequestDetails) throws ResourceNotFoundException { // We only pass 1 input in so only 0..1 will come back - IdDt id = new IdDt(theResourceName, theResourceId); + IdDt id = new IdDt(theResourceType, theResourceId); Collection matches = translateForcedIdToPids(thePartitionId, theRequestDetails, Collections.singletonList(id)); assert matches.size() <= 1; if (matches.isEmpty()) { @@ -344,22 +340,16 @@ public class IdHelperService { if (nextIds.size() > 0) { Collection views; - if (isBlank(nextResourceType)) { - warnAboutUnqualifiedForcedIdResolution(theRequest); + assert isNotBlank(nextResourceType); - // FIXME: deal with partition here - views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextIds); - - } else { - if (thePartitionId != null) { - if (thePartitionId.getPartitionId() != null) { - views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartition(nextResourceType, nextIds, thePartitionId.getPartitionId()); - } else { - views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(nextResourceType, nextIds); - } + if (thePartitionId != null) { + if (thePartitionId.getPartitionId() != null) { + views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartition(nextResourceType, nextIds, thePartitionId.getPartitionId()); } else { - views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextResourceType, nextIds); + views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(nextResourceType, nextIds); } + } else { + views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextResourceType, nextIds); } for (Object[] next : views) { @@ -382,17 +372,6 @@ public class IdHelperService { return retVal; } - private void warnAboutUnqualifiedForcedIdResolution(RequestDetails theRequest) { - StorageProcessingMessage msg = new StorageProcessingMessage() - .setMessage("This search uses unqualified resource IDs (an ID without a resource type). This is less efficient than using a qualified type."); - ourLog.debug(msg.getMessage()); - HookParams params = new HookParams() - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest) - .add(StorageProcessingMessage.class, msg); - JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params); - } - public void clearCache() { myPersistentIdCache.invalidateAll(); myResourceLookupCache.invalidateAll(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java index e77f1cc2ba2..c64f7ca0c11 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java @@ -27,7 +27,6 @@ import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.model.config.PartitionConfig; import ca.uhn.fhir.jpa.model.entity.BasePartitionable; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; -import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; @@ -146,16 +145,23 @@ abstract class BasePredicateBuilder { } Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From theFrom, Predicate thePredicate, PartitionId thePartitionId) { + List andPredicates = new ArrayList<>(); + addPartitionIdPredicate(thePartitionId, theFrom, andPredicates); + if (myDontUseHashesForSearch) { Predicate resourceTypePredicate = myCriteriaBuilder.equal(theFrom.get("myResourceType"), theResourceName); Predicate paramNamePredicate = myCriteriaBuilder.equal(theFrom.get("myParamName"), theParamName); - Predicate outerPredicate = myCriteriaBuilder.and(resourceTypePredicate, paramNamePredicate, thePredicate); - return outerPredicate; + andPredicates.add(resourceTypePredicate); + andPredicates.add(paramNamePredicate); + andPredicates.add(thePredicate); + } else { + long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myPartitionConfig, thePartitionId, theResourceName, theParamName); + Predicate hashIdentityPredicate = myCriteriaBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity); + andPredicates.add(hashIdentityPredicate); + andPredicates.add(thePredicate); } - long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myPartitionConfig, thePartitionId, theResourceName, theParamName); - Predicate hashIdentityPredicate = myCriteriaBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity); - return myCriteriaBuilder.and(hashIdentityPredicate, thePredicate); + return myCriteriaBuilder.and(toArray(andPredicates)); } public PartitionConfig getPartitionConfig() { @@ -217,7 +223,7 @@ abstract class BasePredicateBuilder { return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, num, thePartitionId); } - void addPartitionIdPredicate(PartitionId thePartitionId, Join theJoin, List theCodePredicates) { + void addPartitionIdPredicate(PartitionId thePartitionId, From theJoin, List theCodePredicates) { if (thePartitionId != null) { Integer partitionId = thePartitionId.getPartitionId(); Predicate partitionPredicate; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java index 3e0de109443..b416cfdc807 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java @@ -82,17 +82,13 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi } List codePredicates = new ArrayList<>(); - addPartitionIdPredicate(thePartitionId, join, codePredicates); for (IQueryParameterType nextOr : theList) { - IQueryParameterType params = nextOr; - Predicate p = createPredicateDate(params, - theResourceName, - theParamName, + Predicate p = createPredicateDate(nextOr, myCriteriaBuilder, join, - operation, - thePartitionId); + operation + ); codePredicates.add(p); } @@ -116,22 +112,17 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi From theFrom, PartitionId thePartitionId) { Predicate predicateDate = createPredicateDate(theParam, - theResourceName, - theParamName, theBuilder, theFrom, - null, - thePartitionId); + null + ); return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, predicateDate, thePartitionId); } private Predicate createPredicateDate(IQueryParameterType theParam, - String theResourceName, - String theParamName, CriteriaBuilder theBuilder, From theFrom, - SearchFilterParser.CompareOperation theOperation, - PartitionId thePartitionId) { + SearchFilterParser.CompareOperation theOperation) { Predicate p; if (theParam instanceof DateParam) { @@ -159,15 +150,14 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi return p; } - // FIXME: does this need a partition ID? private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder, From theFrom, DateRangeParam theRange, SearchFilterParser.CompareOperation operation) { Date lowerBound = theRange.getLowerBoundAsInstant(); Date upperBound = theRange.getUpperBoundAsInstant(); - Predicate lt = null; - Predicate gt = null; + Predicate lt; + Predicate gt; Predicate lb = null; Predicate ub = null; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java index e034a80455c..1468bb93629 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java @@ -77,6 +77,7 @@ import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.persistence.criteria.From; import javax.persistence.criteria.Join; import javax.persistence.criteria.JoinType; @@ -193,6 +194,12 @@ class PredicateBuilderReference extends BasePredicateBuilder { List codePredicates = new ArrayList<>(); addPartitionIdPredicate(thePartitionId, join, codePredicates); + for (IIdType next : targetIds) { + if (!next.hasResourceType()) { + warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, null); + } + } + // Resources by ID List targetPids = myIdHelperService.resolveResourcePersistentIdsWithCache(thePartitionId, targetIds, theRequest); if (!targetPids.isEmpty()) { @@ -419,13 +426,20 @@ class PredicateBuilderReference extends BasePredicateBuilder { return predicate; } - private void warnAboutPerformanceOnUnqualifiedResources(String theParamName, RequestDetails theRequest, List> theCandidateTargetTypes) { - String message = new StringBuilder() - .append("This search uses an unqualified resource(a parameter in a chain without a resource type). ") - .append("This is less efficient than using a qualified type. ") - .append("[" + theParamName + "] resolves to [" + theCandidateTargetTypes.stream().map(Class::getSimpleName).collect(Collectors.joining(",")) + "].") - .append("If you know what you're looking for, try qualifying it like this: ") - .append(theCandidateTargetTypes.stream().map(cls -> "[" + cls.getSimpleName() + ":" + theParamName + "]").collect(Collectors.joining(" or "))) + private void warnAboutPerformanceOnUnqualifiedResources(String theParamName, RequestDetails theRequest, @Nullable List> theCandidateTargetTypes) { + StringBuilder builder = new StringBuilder(); + builder.append("This search uses an unqualified resource(a parameter in a chain without a resource type). "); + builder.append("This is less efficient than using a qualified type. "); + if (theCandidateTargetTypes != null) { + builder.append("[" + theParamName + "] resolves to [" + theCandidateTargetTypes.stream().map(Class::getSimpleName).collect(Collectors.joining(",")) + "]."); + builder.append("If you know what you're looking for, try qualifying it using the form "); + builder.append(theCandidateTargetTypes.stream().map(cls -> "[" + cls.getSimpleName() + ":" + theParamName + "]").collect(Collectors.joining(" or "))); + } else { + builder.append("If you know what you're looking for, try qualifying it using the form: '"); + builder.append(theParamName).append(":[resourceType]"); + builder.append("'"); + } + String message = builder .toString(); StorageProcessingMessage msg = new StorageProcessingMessage() .setMessage(message); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java index 9b42b000207..544ba889d8f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java @@ -134,7 +134,6 @@ class PredicateBuilderTag extends BasePredicateBuilder { continue; } - // FIXME: add test for :missing if (paramInverted) { ourLog.debug("Searching for _tag:not"); @@ -160,6 +159,11 @@ class PredicateBuilderTag extends BasePredicateBuilder { defJoin.where(tagListPredicate); continue; + + } else { + + myQueryRoot.setHasIndexJoins(); + } Join tagJoin = myQueryRoot.join("myTags", JoinType.LEFT); @@ -172,7 +176,6 @@ class PredicateBuilderTag extends BasePredicateBuilder { addPartitionIdPredicate(thePartitionId, tagJoin, predicates); } - myQueryRoot.setHasIndexJoins(); myQueryRoot.addPredicates(predicates); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java index faee56bc771..c038fdb1399 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java @@ -104,10 +104,6 @@ public class TestUtil { Subselect subselect = theClazz.getAnnotation(Subselect.class); boolean isView = (subselect != null); - // FIXME: remove? - Embeddable embeddable = theClazz.getAnnotation(Embeddable.class); - boolean isEmbeddable = (embeddable != null); - scan(theClazz, theNames, theIsSuperClass, isView); for (Field nextField : theClazz.getDeclaredFields()) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index f8d1d92ccec..18322e0f197 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -1,6 +1,9 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.interceptor.api.HookParams; +import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.model.config.PartitionConfig; @@ -12,6 +15,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.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; @@ -46,6 +50,8 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; @@ -81,6 +87,8 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @SuppressWarnings({"unchecked", "Duplicates"}) public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { @@ -3507,6 +3515,44 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { } } + + @Test + public void testSearchReferenceUntyped() { + Patient p = new Patient(); + p.setActive(true); + p.setId("PAT"); + myPatientDao.update(p); + + AuditEvent audit = new AuditEvent(); + audit.setId("AUDIT"); + audit.addEntity().getWhat().setReference("Patient/PAT"); + myAuditEventDao.update(audit); + + IAnonymousInterceptor interceptor = mock(IAnonymousInterceptor.class); + try { + myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.JPA_PERFTRACE_WARNING, interceptor); + + myCaptureQueriesListener.clear(); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(AuditEvent.SP_ENTITY, new ReferenceParam("PAT")); + IBundleProvider outcome = myAuditEventDao.search(map); + assertThat(toUnqualifiedVersionlessIdValues(outcome), contains("AuditEvent/AUDIT")); + + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + } finally { + myInterceptorRegistry.unregisterInterceptor(interceptor); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(HookParams.class); + verify(interceptor, times(1)).invoke(ArgumentMatchers.eq(Pointcut.JPA_PERFTRACE_WARNING), captor.capture()); + StorageProcessingMessage message = captor.getValue().get(StorageProcessingMessage.class); + assertEquals("This search uses an unqualified resource(a parameter in a chain without a resource type). This is less efficient than using a qualified type. If you know what you're looking for, try qualifying it using the form: 'entity:[resourceType]'", message.getMessage()); + } + + @Test public void testSearchWithDateAndReusesExistingJoin() { // Add a search parameter to Observation.issued, so that between that one diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java index fc2108eec2b..555b9a1651d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java @@ -12,7 +12,10 @@ import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.DateAndListParam; +import ca.uhn.fhir.rest.param.DateOrListParam; import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; @@ -1156,7 +1159,239 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); } - // FIXME: add DATE and DATE RANGE test + @Test + public void testSearch_DateParam_SearchAllPartitions() { + myPartitionConfig.setIncludePartitionInSearchHashes(false); + + IIdType patientIdNull = createPatient(null, withBirthdate("2020-04-20")); + IIdType patientId1 = createPatient(1, withBirthdate("2020-04-20")); + IIdType patientId2 = createPatient(2, withBirthdate("2020-04-20")); + createPatient(null, withBirthdate("2021-04-20")); + createPatient(1, withBirthdate("2021-04-20")); + createPatient(2, withBirthdate("2021-04-20")); + + // Date param + + addReadPartition(null); + myCaptureQueriesListener.clear(); + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateParam("2020-04-20")); + map.setLoadSynchronous(true); + IBundleProvider results = myPatientDao.search(map); + List ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + // Date OR param + + addReadPartition(null); + myCaptureQueriesListener.clear(); + map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateOrListParam().addOr(new DateParam("2020-04-20")).addOr(new DateParam("2020-04-22"))); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + + searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + // Date AND param + + addReadPartition(null); + myCaptureQueriesListener.clear(); + map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateAndListParam().addAnd(new DateOrListParam().addOr(new DateParam("2020"))).addAnd(new DateOrListParam().addOr(new DateParam("2020-04-20")))); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + + searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + // DateRangeParam + + addReadPartition(null); + myCaptureQueriesListener.clear(); + map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateRangeParam(new DateParam("2020-01-01"), new DateParam("2020-04-25"))); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + + searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + } + + + @Test + public void testSearch_DateParam_SearchSpecificPartitions() { + myPartitionConfig.setIncludePartitionInSearchHashes(false); + + IIdType patientIdNull = createPatient(null, withBirthdate("2020-04-20")); + IIdType patientId1 = createPatient(1, withBirthdate("2020-04-20")); + IIdType patientId2 = createPatient(2, withBirthdate("2020-04-20")); + createPatient(null, withBirthdate("2021-04-20")); + createPatient(1, withBirthdate("2021-04-20")); + createPatient(2, withBirthdate("2021-04-20")); + + // Date param + + addReadPartition(1); + myCaptureQueriesListener.clear(); + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateParam("2020-04-20")); + map.setLoadSynchronous(true); + IBundleProvider results = myPatientDao.search(map); + List ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientId1)); + + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + // Date OR param + + addReadPartition(1); + myCaptureQueriesListener.clear(); + map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateOrListParam().addOr(new DateParam("2020-04-20")).addOr(new DateParam("2020-04-22"))); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientId1)); + + searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + // Date AND param + + addReadPartition(1); + myCaptureQueriesListener.clear(); + map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateAndListParam().addAnd(new DateOrListParam().addOr(new DateParam("2020"))).addAnd(new DateOrListParam().addOr(new DateParam("2020-04-20")))); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientId1)); + + searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + // DateRangeParam + + addReadPartition(1); + myCaptureQueriesListener.clear(); + map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateRangeParam(new DateParam("2020-01-01"), new DateParam("2020-04-25"))); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientId1)); + + searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + } + + + @Test + public void testSearch_DateParam_SearchDefaultPartitions() { + myPartitionConfig.setIncludePartitionInSearchHashes(false); + + IIdType patientIdNull = createPatient(null, withBirthdate("2020-04-20")); + IIdType patientId1 = createPatient(1, withBirthdate("2020-04-20")); + IIdType patientId2 = createPatient(2, withBirthdate("2020-04-20")); + createPatient(null, withBirthdate("2021-04-20")); + createPatient(1, withBirthdate("2021-04-20")); + createPatient(2, withBirthdate("2021-04-20")); + + // Date param + + addDefaultReadPartition(); + myCaptureQueriesListener.clear(); + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateParam("2020-04-20")); + map.setLoadSynchronous(true); + IBundleProvider results = myPatientDao.search(map); + List ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientIdNull)); + + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + // Date OR param + + addDefaultReadPartition(); + myCaptureQueriesListener.clear(); + map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateOrListParam().addOr(new DateParam("2020-04-20")).addOr(new DateParam("2020-04-22"))); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientIdNull)); + + searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + // Date AND param + + addDefaultReadPartition(); + myCaptureQueriesListener.clear(); + map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateAndListParam().addAnd(new DateOrListParam().addOr(new DateParam("2020"))).addAnd(new DateOrListParam().addOr(new DateParam("2020-04-20")))); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientIdNull)); + + searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + // DateRangeParam + + addDefaultReadPartition(); + myCaptureQueriesListener.clear(); + map = new SearchParameterMap(); + map.add(Patient.SP_BIRTHDATE, new DateRangeParam(new DateParam("2020-01-01"), new DateParam("2020-04-25"))); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientIdNull)); + + searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW")); + + } + @Test public void testSearch_StringParam_SearchAllPartitions() { @@ -1298,6 +1533,80 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_NORMALIZED")); } + @Test + public void testSearch_TagNotParam_SearchAllPartitions() { + IIdType patientIdNull = createPatient(null, withActiveTrue(), withTag("http://system", "code")); + IIdType patientId1 = createPatient(1, withActiveTrue(), withTag("http://system", "code")); + IIdType patientId2 = createPatient(2, withActiveTrue(), withTag("http://system", "code")); + createPatient(null, withActiveTrue(), withTag("http://system", "code2")); + createPatient(1, withActiveTrue(), withTag("http://system", "code2")); + createPatient(2, withActiveTrue(), withTag("http://system", "code2")); + + addReadPartition(null); + + myCaptureQueriesListener.clear(); + SearchParameterMap map = new SearchParameterMap(); + map.add(Constants.PARAM_TAG, new TokenParam("http://system", "code2").setModifier(TokenParamModifier.NOT)); + map.setLoadSynchronous(true); + IBundleProvider results = myPatientDao.search(map); + List ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2)); + + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(1, StringUtils.countMatches(searchSql, "TAG_SYSTEM='http://system'")); + } + + @Test + public void testSearch_TagNotParam_SearchDefaultPartition() { + IIdType patientIdNull = createPatient(null, withActiveTrue(), withTag("http://system", "code")); + createPatient(1, withActiveTrue(), withTag("http://system", "code")); + createPatient(2, withActiveTrue(), withTag("http://system", "code")); + + addDefaultReadPartition(); + + myCaptureQueriesListener.clear(); + SearchParameterMap map = new SearchParameterMap(); + map.add(Constants.PARAM_TAG, new TokenParam("http://system", "code2").setModifier(TokenParamModifier.NOT)); + map.setLoadSynchronous(true); + IBundleProvider results = myPatientDao.search(map); + List ids = toUnqualifiedVersionlessIds(results); + + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID is null")); + assertEquals(1, StringUtils.countMatches(searchSql, "TAG_SYSTEM='http://system'")); + + assertThat(ids.toString(), ids, Matchers.contains(patientIdNull)); + } + + @Test + public void testSearch_TagNotParam_SearchOnePartition() { + createPatient(null, withActiveTrue(), withTag("http://system", "code")); + IIdType patientId1 = createPatient(1, withActiveTrue(), withTag("http://system", "code")); + createPatient(2, withActiveTrue(), withTag("http://system", "code")); + createPatient(null, withActiveTrue(), withTag("http://system", "code2")); + createPatient(1, withActiveTrue(), withTag("http://system", "code2")); + createPatient(2, withActiveTrue(), withTag("http://system", "code2")); + + addReadPartition(1); + + myCaptureQueriesListener.clear(); + SearchParameterMap map = new SearchParameterMap(); + map.add(Constants.PARAM_TAG, new TokenParam("http://system", "code2").setModifier(TokenParamModifier.NOT)); + map.setLoadSynchronous(true); + IBundleProvider results = myPatientDao.search(map); + List ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientId1)); + + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(1, StringUtils.countMatches(searchSql, "TAG_SYSTEM='http://system'")); + } + @Test public void testSearch_TagParam_SearchAllPartitions() { IIdType patientIdNull = createPatient(null, withActiveTrue(), withTag("http://system", "code")); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java index d4bb03ff99b..b1e98fbe436 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java @@ -3,7 +3,7 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.jpa.model.config.PartitionConfig; import ca.uhn.fhir.jpa.model.util.ProviderConstants; import ca.uhn.fhir.jpa.partition.PartitionManagementProvider; -import ca.uhn.fhir.jpa.partition.RequestTenantPartitionInterceptor; +import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; import ca.uhn.fhir.rest.client.interceptor.UrlTenantSelectionInterceptor; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java index 86a0f40c7fc..46f2779470d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java @@ -688,9 +688,8 @@ public class SystemProviderR4Test extends BaseJpaR4Test { " \n" + " \n" + " \n" + - // FIXME: restore -// " \n" + -// " \n" + + " \n" + + " \n" + " \n" + " \n" + " \n" + @@ -710,9 +709,6 @@ public class SystemProviderR4Test extends BaseJpaR4Test { HttpPost req = new HttpPost(ourServerBase); req.setEntity(new StringEntity(input, ContentType.parse(Constants.CT_FHIR_XML + "; charset=utf-8"))); - // FIXME: remove - myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); - CloseableHttpResponse resp = ourHttpClient.execute(req); try { String encoded = IOUtils.toString(resp.getEntity().getContent(), StandardCharsets.UTF_8); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java index 1cbde28be03..356440efa8b 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java @@ -201,6 +201,7 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar return b.build(); } + @SuppressWarnings("ConstantConditions") @Override public boolean matches(IQueryParameterType theParam) { if (!(theParam instanceof DateParam)) { @@ -216,7 +217,6 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar return false; } - // FIXME: below is always true boolean result = true; if (lowerBound != null) { result &= (myValueLow.after(lowerBound) || myValueLow.equals(lowerBound)); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantPartitionInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.java similarity index 68% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantPartitionInterceptor.java rename to hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.java index 649f34d94b1..3c71ce5414f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantPartitionInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.partition; +package ca.uhn.fhir.rest.server.interceptor.partition; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR - Server Framework * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -26,19 +26,28 @@ import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.PartitionId; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.jetbrains.annotations.NotNull; +import ca.uhn.fhir.rest.server.tenant.ITenantIdentificationStrategy; + +import javax.annotation.Nonnull; import static org.apache.commons.lang3.StringUtils.isBlank; /** + * This interceptor uses the request tenant ID (as supplied to the server using + * {@link ca.uhn.fhir.rest.server.RestfulServer#setTenantIdentificationStrategy(ITenantIdentificationStrategy)} + * to indicate the partition ID. With this interceptor registered, The server treats the tenant name + * supplied by the {@link ITenantIdentificationStrategy tenant identification strategy} as a partition name. + *

+ * Partition names (aka tenant IDs) must be registered in advance using the partition management operations. + *

* + * @since 5.0.0 */ @Interceptor public class RequestTenantPartitionInterceptor { @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE) - public PartitionId PartitionIdentifyCreate(IBaseResource theResource, ServletRequestDetails theRequestDetails) { + public PartitionId PartitionIdentifyCreate(ServletRequestDetails theRequestDetails) { return extractPartitionIdFromRequest(theRequestDetails); } @@ -47,7 +56,7 @@ public class RequestTenantPartitionInterceptor { return extractPartitionIdFromRequest(theRequestDetails); } - @NotNull + @Nonnull private PartitionId extractPartitionIdFromRequest(ServletRequestDetails theRequestDetails) { // We will use the tenant ID that came from the request as the partition name diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/tenant/UrlBaseTenantIdentificationStrategy.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/tenant/UrlBaseTenantIdentificationStrategy.java index 2dfba738997..1558224d7d6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/tenant/UrlBaseTenantIdentificationStrategy.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/tenant/UrlBaseTenantIdentificationStrategy.java @@ -33,7 +33,16 @@ import static org.apache.commons.lang3.StringUtils.defaultIfBlank; /** * This class is a tenant identification strategy which assumes that a single path - * element will be present between the server base URL and the beginning + * element will be present between the server base URL and individual request. + *

+ * For example, + * with this strategy enabled, given the following URL on a server with base URL http://example.com/base, + * the server will extract the TENANT-A portion of the URL and use it as the tenant identifier. The + * request will then proceed to read the resource with ID Patient/123. + *

+ *

+ * GET http://example.com/base/TENANT-A/Patient/123 + *

*/ public class UrlBaseTenantIdentificationStrategy implements ITenantIdentificationStrategy {