Merge remote-tracking branch 'remotes/origin/master' into im_20200316_lastn_operation_elasticsearch

# Conflicts:
#	hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java
This commit is contained in:
ianmarshall 2020-04-24 16:55:45 -04:00
commit dd749791db
249 changed files with 8453 additions and 1905 deletions

View File

@ -6,7 +6,7 @@ HAPI FHIR - Java API for HL7 FHIR Clients and Servers
[![Build Status](https://dev.azure.com/jamesagnew214/jamesagnew214/_apis/build/status/jamesagnew.hapi-fhir?branchName=master)](https://dev.azure.com/jamesagnew214/jamesagnew214/_build/latest?definitionId=1&branchName=master)
[![codecov](https://codecov.io/gh/jamesagnew/hapi-fhir/branch/master/graph/badge.svg)](https://codecov.io/gh/jamesagnew/hapi-fhir)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg)](http://search.maven.org/#search|ga|1|ca.uhn.hapi.fhir)
[![License](https://img.shields.io/badge/license-apache%202.0-60C060.svg)](http://jamesagnew.github.io/hapi-fhir/license.html)
[![License](https://img.shields.io/badge/license-apache%202.0-60C060.svg)](https://hapifhir.io/hapi-fhir/license.html)
Complete project documentation is available here:
http://hapifhir.io

View File

@ -40,10 +40,8 @@
</p>
</th:block>
<p>
<b style="color: red;">
<span class="glyphicon glyphicon-warning-sign"/>
This is not a production server!
</b>
<b class="text-danger"><span class="glyphicon glyphicon-warning-sign"/></b>
<b><span class="text-danger">This is not a production server!</span></b>
Do not store any information here that contains personal health information
or any other confidential information. This server will be regularly purged
and reloaded with fixed test data.

View File

@ -40,10 +40,8 @@
</p>
</th:block>
<p>
<b style="color: red;">
<span class="glyphicon glyphicon-warning-sign"/>
This is not a production server!
</b>
<b class="text-danger"><span class="glyphicon glyphicon-warning-sign"/></b>
<b><span class="text-danger">This is not a production server!</span></b>
Do not store any information here that contains personal health information
or any other confidential information. This server will be regularly purged
and reloaded with fixed test data.

View File

@ -40,10 +40,8 @@
</p>
</th:block>
<p>
<b style="color: red;">
<span class="glyphicon glyphicon-warning-sign"/>
This is not a production server!
</b>
<b class="text-danger"><span class="glyphicon glyphicon-warning-sign"/></b>
<b><span class="text-danger">This is not a production server!</span></b>
Do not store any information here that contains personal health information
or any other confidential information. This server will be regularly purged
and reloaded with fixed test data.

View File

@ -695,7 +695,7 @@ public class FhirContext {
* cases to contain methods for each of the RESTful operations you wish to implement (e.g. "read ImagingStudy",
* "search Patient by identifier", etc.). This interface must extend {@link IRestfulClient} (or commonly its
* sub-interface {@link IBasicClient}). See the <a
* href="http://jamesagnew.github.io/hapi-fhir/doc_rest_client.html">RESTful Client</a> documentation for more
* href="https://hapifhir.io/hapi-fhir/docs/client/introduction.html">RESTful Client</a> documentation for more
* information on how to define this interface.
*
* <p>

View File

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

View File

@ -23,6 +23,8 @@ package ca.uhn.fhir.interceptor.api;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
public interface IInterceptorService extends IInterceptorBroadcaster {
@ -90,4 +92,8 @@ public interface IInterceptorService extends IInterceptorBroadcaster {
void registerInterceptors(@Nullable Collection<?> theInterceptors);
/**
* Unregisters all interceptors that are indicated by the given callback function returning <code>true</code>
*/
void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction);
}

View File

@ -65,12 +65,16 @@ public enum Pointcut {
* <li>
* ca.uhn.fhir.rest.client.api.IHttpRequest - The details of the request
* </li>
* <li>
* ca.uhn.fhir.rest.client.api.IRestfulClient - The client object making the request
* </li>
* </ul>
* </p>
* Hook methods must return <code>void</code>.
*/
CLIENT_REQUEST(void.class,
"ca.uhn.fhir.rest.client.api.IHttpRequest"
"ca.uhn.fhir.rest.client.api.IHttpRequest",
"ca.uhn.fhir.rest.client.api.IRestfulClient"
),
/**
@ -82,7 +86,12 @@ public enum Pointcut {
* <ul>
* <li>
* ca.uhn.fhir.rest.client.api.IHttpRequest - The details of the request
* ca.uhn.fhir.rest.client.api.IHttpRequest - The details of the response
* </li>
* <li>
* ca.uhn.fhir.rest.client.api.IHttpResponse - The details of the response
* </li>
* <li>
* ca.uhn.fhir.rest.client.api.IRestfulClient - The client object making the request
* </li>
* </ul>
* </p>
@ -90,7 +99,8 @@ public enum Pointcut {
*/
CLIENT_RESPONSE(void.class,
"ca.uhn.fhir.rest.client.api.IHttpRequest",
"ca.uhn.fhir.rest.client.api.IHttpResponse"
"ca.uhn.fhir.rest.client.api.IHttpResponse",
"ca.uhn.fhir.rest.client.api.IRestfulClient"
),
/**
@ -1248,7 +1258,7 @@ public enum Pointcut {
/**
* <b>Storage Hook:</b>
* Invoked when a resource delete operation is about to fail due to referential integrity conflicts.
* Invoked when a resource delete operation is about to fail due to referential integrity hcts.
* <p>
* Hooks will have access to the list of resources that have references to the resource being deleted.
* </p>
@ -1364,6 +1374,83 @@ public enum Pointcut {
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* <b>Storage Hook:</b>
* Invoked before FHIR <b>create</b> operation to request the identification of the partition ID to be associated
* with the resource being created. This hook will only be called if partitioning is enabled in the JPA
* server.
* <p>
* Hooks may accept the following parameters:
* </p>
* <ul>
* <li>
* org.hl7.fhir.instance.model.api.IBaseResource - The resource that will be created and needs a tenant ID assigned.
* </li>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks should return an instance of <code>ca.uhn.fhir.jpa.api.model.RequestPartitionId</code> or <code>null</code>.
* </p>
*/
STORAGE_PARTITION_IDENTIFY_CREATE(
// Return type
"ca.uhn.fhir.interceptor.model.RequestPartitionId",
// Params
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* <b>Storage Hook:</b>
* Invoked before FHIR read/access operation (e.g. <b>read/vread</b>, <b>search</b>, <b>history</b>, etc.) operation to request the
* identification of the partition ID to be associated with the resource(s) being searched for, read, etc.
* <p>
* This hook will only be called if
* partitioning is enabled in the JPA server.
* </p>
* <p>
* Hooks may accept the following parameters:
* </p>
* <ul>
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks should return an instance of <code>ca.uhn.fhir.jpa.api.model.RequestPartitionId</code> or <code>null</code>.
* </p>
*/
STORAGE_PARTITION_IDENTIFY_READ(
// Return type
"ca.uhn.fhir.interceptor.model.RequestPartitionId",
// Params
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* <b>Performance Tracing Hook:</b>
* This hook is invoked when any informational messages generated by the

View File

@ -20,7 +20,13 @@ package ca.uhn.fhir.interceptor.executor;
* #L%
*/
import ca.uhn.fhir.interceptor.api.*;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
@ -41,6 +47,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class InterceptorService implements IInterceptorService, IInterceptorBroadcaster {
@ -145,6 +152,16 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
}
}
@Override
public void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction) {
unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers);
unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers);
}
private void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction, ListMultimap<Pointcut, BaseInvoker> theGlobalInvokers) {
theGlobalInvokers.entries().removeIf(t->theShouldUnregisterFunction.test(t.getValue().getInterceptor()));
}
@Override
public boolean registerThreadLocalInterceptor(Object theInterceptor) {
if (!myThreadlocalInvokersEnabled) {

View File

@ -0,0 +1,106 @@
package ca.uhn.fhir.interceptor.model;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* 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 javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.LocalDate;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
public class RequestPartitionId {
private final Integer myPartitionId;
private final LocalDate myPartitionDate;
private final String myPartitionName;
/**
* Constructor
*/
private RequestPartitionId(@Nullable String thePartitionName, @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) {
myPartitionName = thePartitionName;
myPartitionId = thePartitionId;
myPartitionDate = thePartitionDate;
}
public String getPartitionName() {
return myPartitionName;
}
@Nullable
public Integer getPartitionId() {
return myPartitionId;
}
@Nullable
public LocalDate getPartitionDate() {
return myPartitionDate;
}
@Override
public String toString() {
return getPartitionIdStringOrNullString();
}
/**
* Returns the partition ID (numeric) as a string, or the string "null"
*/
public String getPartitionIdStringOrNullString() {
return defaultIfNull(myPartitionId, "null").toString();
}
/**
* Create a string representation suitable for use as a cache key. Null aware.
*/
public static String stringifyForKey(RequestPartitionId theRequestPartitionId) {
String retVal = "(null)";
if (theRequestPartitionId != null) {
retVal = theRequestPartitionId.getPartitionIdStringOrNullString();
}
return retVal;
}
@Nonnull
public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId) {
return fromPartitionId(thePartitionId, null);
}
@Nonnull
public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) {
return new RequestPartitionId(null, thePartitionId, thePartitionDate);
}
@Nonnull
public static RequestPartitionId fromPartitionName(@Nullable String thePartitionName) {
return fromPartitionName(thePartitionName, null);
}
@Nonnull
public static RequestPartitionId fromPartitionName(@Nullable String thePartitionName, @Nullable LocalDate thePartitionDate) {
return new RequestPartitionId(thePartitionName, null, thePartitionDate);
}
@Nonnull
public static RequestPartitionId forPartitionNameAndId(@Nullable String thePartitionName, @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) {
return new RequestPartitionId(thePartitionName, thePartitionId, thePartitionDate);
}
}

View File

@ -1290,10 +1290,22 @@ class ParserState<T> {
ParseLocation location = ParseLocation.fromElementName(myChildName);
myErrorHandler.invalidValue(location, value, "Attribute value must not be empty (\"\")");
} else {
/*
* It may be possible to clean this up somewhat once the following PR is hopefully merged:
* https://github.com/FasterXML/jackson-core/pull/611
*
* See TolerantJsonParser
*/
if ("decimal".equals(myTypeName)) {
if (value != null && value.startsWith(".") && NumberUtils.isDigits(value.substring(1))) {
value = "0" + value;
}
if (value != null)
if (value.startsWith(".") && NumberUtils.isDigits(value.substring(1))) {
value = "0" + value;
} else {
while (value.startsWith("00")) {
value = value.substring(1);
}
}
}
try {
@ -1324,17 +1336,6 @@ class ParserState<T> {
pop();
}
// @Override
// public void enteringNewElementExtension(StartElement theElement,
// String theUrlAttr) {
// if (myInstance instanceof ISupportsUndeclaredExtensions) {
// UndeclaredExtension ext = new UndeclaredExtension(theUrlAttr);
// ((ISupportsUndeclaredExtensions)
// myInstance).getUndeclaredExtensions().add(ext);
// push(new ExtensionState(ext));
// }
// }
@Override
public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
super.enteringNewElement(theNamespaceUri, theLocalPart);

View File

@ -48,7 +48,7 @@ import ca.uhn.fhir.model.primitive.IdDt;
* Note that for a
* server implementation, the {@link #type()} annotation is optional if the
* method is defined in a <a href=
* "http://jamesagnew.github.io/hapi-fhir/doc_rest_server.html#resource_providers"
* "https://hapifhir.io/hapi-fhir/docs/server_plain/resource_providers.html"
* >resource provider</a>, since the type is implied.</li>
* <li>
* To add tag(s) on the server <b>to the given version of the
@ -63,7 +63,7 @@ import ca.uhn.fhir.model.primitive.IdDt;
* operation.
* Note that for a server implementation, the
* {@link #type()} annotation is optional if the method is defined in a <a href=
* "http://jamesagnew.github.io/hapi-fhir/doc_rest_server.html#resource_providers"
* "https://hapifhir.io/hapi-fhir/docs/server_plain/resource_providers.html"
* >resource provider</a>, since the type is implied.</li>
* </ul>
*/

View File

@ -46,7 +46,7 @@ import ca.uhn.fhir.model.primitive.IdDt;
* Note that for a
* server implementation, the {@link #type()} annotation is optional if the
* method is defined in a <a href=
* "http://jamesagnew.github.io/hapi-fhir/doc_rest_server.html#resource_providers"
* "https://hapifhir.io/hapi-fhir/docs/server_plain/resource_providers.html"
* >resource provider</a>, since the type is implied.</li>
* <li>
* To delete tag(s) on the server <b>to the given version of the
@ -59,7 +59,7 @@ import ca.uhn.fhir.model.primitive.IdDt;
* to be deleted.
* Note that for a server implementation, the
* {@link #type()} annotation is optional if the method is defined in a <a href=
* "http://jamesagnew.github.io/hapi-fhir/doc_rest_server.html#resource_providers"
* "https://hapifhir.io/hapi-fhir/docs/server_plain/resource_providers.html"
* >resource provider</a>, since the type is implied.</li>
* </ul>
*/

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.subscription.match.registry;
package ca.uhn.fhir.rest.api;
/*-
* #%L
* HAPI FHIR Subscription Server
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,12 +20,16 @@ package ca.uhn.fhir.jpa.subscription.match.registry;
* #L%
*/
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
/**
* Used by the client to indicate the cascade mode associated with a delete operation.
* <p>
* Note that this is a HAPI FHIR specific feature, and may not work on other platforms.
* </p>
*/
public enum DeleteCascadeModeEnum {
public interface ISubscriptionProvider {
IBundleProvider search(SearchParameterMap theMap);
NONE,
DELETE
boolean loadSubscription(IBaseResource theResource);
}

View File

@ -29,7 +29,7 @@ import java.io.IOException;
* This interface represents an interceptor which can be used to access (and optionally change or take actions upon)
* requests that are being sent by the HTTP client, and responses received by it.
* <p>
* See the <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_client_interceptor.html">HAPI Documentation Client Interceptor</a>
* See the <a href="https://hapifhir.io/hapi-fhir/docs/interceptors/client_interceptors.html">HAPI Documentation Client Interceptor</a>
* page for more information on how to use this feature.
* </p>
*/

View File

@ -63,6 +63,11 @@ public interface IHttpRequest {
*/
String getUri();
/**
* Modify the request URI, or null
*/
void setUri(String theUrl);
/**
* Return the HTTP verb (e.g. "GET")
*/

View File

@ -137,7 +137,7 @@ public class DateClientParam extends BaseClientParam implements IParam {
myPrefix = thePrefix;
this.previous = previous;
}
public DateWithPrefix(ParamPrefixEnum thePrefix) {
myPrefix = thePrefix;
}
@ -176,13 +176,27 @@ public class DateClientParam extends BaseClientParam implements IParam {
dt.setPrecision(TemporalPrecisionEnum.SECOND);
return constructCriterion(dt);
}
@Override
public IDateCriterion millis(Date theValue) {
DateTimeDt dt = new DateTimeDt(theValue);
dt.setPrecision(TemporalPrecisionEnum.MILLI);
return constructCriterion(dt);
}
@Override
public IDateCriterion millis(String theValue) {
DateTimeDt dt = new DateTimeDt(theValue);
dt.setPrecision(TemporalPrecisionEnum.MILLI);
return constructCriterion(dt);
}
private IDateCriterion constructCriterion(DateTimeDt dt) {
String valueAsString = dt.getValueAsString();
Criterion criterion = new Criterion(myPrefix, valueAsString);
if (previous != null) {
criterion.orCriterion = previous;
}
}
return criterion;
}
}
@ -199,8 +213,12 @@ public class DateClientParam extends BaseClientParam implements IParam {
IDateCriterion second(String theValue);
IDateCriterion millis(Date theValue);
IDateCriterion millis(String theValue);
}
public interface IDateCriterion extends ICriterion<DateClientParam> {
IDateSpecifier orAfter();

View File

@ -35,7 +35,7 @@ public interface IClientExecutable<T extends IClientExecutable<?, Y>, Y> {
* If set to true, the client will log the request and response to the SLF4J logger. This can be useful for
* debugging, but is generally not desirable in a production situation.
*
* @deprecated Use the client logging interceptor to log requests and responses instead. See <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_client.html#req_resp_logging">here</a> for more information.
* @deprecated Use the client logging interceptor to log requests and responses instead. See <a href="https://hapifhir.io/hapi-fhir/docs/interceptors/built_in_client_interceptors.html">here</a> for more information.
*/
@Deprecated
T andLogRequestAndResponse(boolean theLogRequestAndResponse);

View File

@ -20,10 +20,15 @@ package ca.uhn.fhir.rest.gclient;
* #L%
*/
import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
public interface IDeleteTyped extends IClientExecutable<IDeleteTyped, IBaseOperationOutcome> {
// nothing for now
public interface IDeleteTyped extends IClientExecutable<IDeleteTyped, MethodOutcome> {
/**
* Delete cascade mode - Note that this is a HAPI FHIR specific feature and is not supported on all servers.
*/
IDeleteTyped cascade(DeleteCascadeModeEnum theDelete);
}

View File

@ -72,6 +72,7 @@ public interface IQuery<Y> extends IBaseQuery<IQuery<Y>>, IClientExecutable<IQue
* on a single page.
*
* @deprecated This parameter is badly named, since FHIR calls this parameter "_count" and not "_limit". Use {@link #count(int)} instead (it also sets the _count parameter)
* @see #count(int)
*/
@Deprecated
IQuery<Y> limitTo(int theLimitTo);

View File

@ -29,10 +29,7 @@ public interface IReadExecutable<T extends IBaseResource> extends IClientExecuta
* that the server return an "HTTP 301 Not Modified" if the newest version of the resource
* on the server has the same version as the version ID specified by <code>theVersion</code>.
* In this case, the client operation will perform the linked operation.
* <p>
* See the <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_etag.html">ETag Documentation</a>
* for more information.
* </p>
*
* @param theVersion The version ID (e.g. "123")
*/
IReadIfNoneMatch<T> ifVersionMatches(String theVersion);

View File

@ -22,16 +22,17 @@ package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.util.CoverageIgnore;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.math.BigDecimal;
import static ca.uhn.fhir.model.primitive.IdDt.isValidLong;
import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/ {
@ -78,6 +79,17 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
setValueAsQueryToken(null, null, qualifier, theValue);
}
/**
* Constructor
*
* @since 5.0.0
*/
public ReferenceParam(IIdType theValue) {
if (theValue != null) {
setValueAsQueryToken(null, null, null, theValue.getValue());
}
}
@Override
String doGetQueryParameterQualifier() {
StringBuilder b = new StringBuilder();
@ -209,7 +221,7 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
*/
public ReferenceParam setValue(String theValue) {
IdDt id = new IdDt(theValue);
String qualifier= null;
String qualifier = null;
if (id.hasResourceType()) {
qualifier = ":" + id.getResourceType();
}
@ -231,7 +243,7 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
* to {@link DateParam}. This is useful if you are using reference parameters and want to handle
* chained parameters of different types in a single method.
* <p>
* See <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_operations.html#dynamic_chains">Dynamic Chains</a>
* See <a href="https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_search.html#chained-resource-references">Dynamic Chains</a>
* in the HAPI FHIR documentation for an example of how to use this method.
* </p>
*/
@ -246,7 +258,7 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
* to {@link NumberParam}. This is useful if you are using reference parameters and want to handle
* chained parameters of different types in a single method.
* <p>
* See <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_operations.html#dynamic_chains">Dynamic Chains</a>
* See <a href="https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_search.html#chained-resource-references">Dynamic Chains</a>
* in the HAPI FHIR documentation for an example of how to use this method.
* </p>
*/
@ -261,7 +273,7 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
* to {@link QuantityParam}. This is useful if you are using reference parameters and want to handle
* chained parameters of different types in a single method.
* <p>
* See <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_operations.html#dynamic_chains">Dynamic Chains</a>
* See <a href="https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_search.html#chained-resource-references">Dynamic Chains</a>
* in the HAPI FHIR documentation for an example of how to use this method.
* </p>
*/
@ -286,7 +298,7 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
* to {@link StringParam}. This is useful if you are using reference parameters and want to handle
* chained parameters of different types in a single method.
* <p>
* See <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_operations.html#dynamic_chains">Dynamic Chains</a>
* See <a href="https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_search.html#chained-resource-references">Dynamic Chains</a>
* in the HAPI FHIR documentation for an example of how to use this method.
* </p>
*/
@ -301,7 +313,7 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
* to {@link TokenParam}. This is useful if you are using reference parameters and want to handle
* chained parameters of different types in a single method.
* <p>
* See <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_operations.html#dynamic_chains">Dynamic Chains</a>
* See <a href="https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_search.html#chained-resource-references">Dynamic Chains</a>
* in the HAPI FHIR documentation for an example of how to use this method.
* </p>
*/

View File

@ -30,6 +30,9 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
/**
* Utilities for dealing with parameters resources in a version indepenedent way
@ -37,12 +40,26 @@ import java.util.Optional;
public class ParametersUtil {
public static List<String> getNamedParameterValuesAsString(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) {
Function<IPrimitiveType<?>, String> mapper = t -> defaultIfBlank(t.getValueAsString(), null);
return extractNamedParameters(theCtx, theParameters, theParameterName, mapper);
}
public static List<Integer> getNamedParameterValuesAsInteger(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) {
Function<IPrimitiveType<?>, Integer> mapper = t -> (Integer)t.getValue();
return extractNamedParameters(theCtx, theParameters, theParameterName, mapper);
}
public static Optional<Integer> getNamedParameterValueAsInteger(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) {
return getNamedParameterValuesAsInteger(theCtx, theParameters, theParameterName).stream().findFirst();
}
private static <T> List<T> extractNamedParameters(FhirContext theCtx, IBaseParameters theParameters, String theParameterName, Function<IPrimitiveType<?>, T> theMapper) {
Validate.notNull(theParameters, "theParameters must not be null");
RuntimeResourceDefinition resDef = theCtx.getResourceDefinition(theParameters.getClass());
BaseRuntimeChildDefinition parameterChild = resDef.getChildByName("parameter");
List<IBase> parameterReps = parameterChild.getAccessor().getValues(theParameters);
List<String> retVal = new ArrayList<>();
List<T> retVal = new ArrayList<>();
for (IBase nextParameter : parameterReps) {
BaseRuntimeElementCompositeDefinition<?> nextParameterDef = (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(nextParameter.getClass());
@ -62,12 +79,12 @@ public class ParametersUtil {
valueValues
.stream()
.filter(t -> t instanceof IPrimitiveType<?>)
.map(t -> ((IPrimitiveType<?>) t).getValueAsString())
.filter(StringUtils::isNotBlank)
.map(t->((IPrimitiveType<?>) t))
.map(theMapper)
.filter(t -> t != null)
.forEach(retVal::add);
}
return retVal;
}

View File

@ -28,7 +28,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
* An individual validation module, which applies validation rules against
* resources and adds failure/informational messages as it goes.
*
* See <a href="http://jamesagnew.github.io/hapi-fhir/doc_validation.html">Validation</a>
* See <a href="https://hapifhir.io/hapi-fhir/docs/validation/introduction.html">Validation</a>
* for a list of available modules. You may also create your own.
*/
public interface IValidatorModule {

View File

@ -83,6 +83,7 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.transactionContainsMultipleWithDuplica
ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.transactionEntryHasInvalidVerb=Transaction bundle entry has missing or invalid HTTP Verb specified in Bundle.entry({1}).request.method. Found value: "{0}"
ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.transactionMissingUrl=Unable to perform {0}, no URL provided.
ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.transactionInvalidUrl=Unable to perform {0}, URL provided is invalid: {1}
ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.noSystemOrTypeHistoryForPartitionAwareServer=Type- and Server- level history operation not supported on partitioned server
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.cantValidateWithNoResource=No resource supplied for $validate operation (resource is required unless mode is \"delete\")
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.deleteBlockedBecauseDisabled=Resource deletion is not permitted on this server
@ -138,5 +139,24 @@ ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.expansionTooLarge=Expansion of ValueSet
ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils.failedToApplyPatch=Failed to apply JSON patch to {0}: {1}
ca.uhn.fhir.jpa.graphql.JpaStorageServices.invalidGraphqlArgument=Unknown GraphQL argument "{0}". Value GraphQL argument for this type are: {1}
ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.blacklistedResourceTypeForPartitioning=Resource type {0} can not be partitioned
ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.unknownPartitionId=Unknown partition ID: {0}
ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.unknownPartitionName=Unknown partition name: {0}
ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference.invalidTargetTypeForChain=Resource type "{0}" is not a valid target type for reference search parameter: {1}
ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference.invalidResourceType=Invalid/unsupported resource type: "{0}"
ca.uhn.fhir.jpa.dao.index.IdHelperService.nonUniqueForcedId=Non-unique ID specified, can not process request
ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.missingPartitionIdOrName=Partition must have an ID and a Name
ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantCreatePartition0=Can not create a partition with ID 0 (this is a reserved value)
ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.unknownPartitionId=No partition exists with ID {0}
ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.invalidName=Partition name "{0}" is not valid
ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantCreateDuplicatePartitionName=Partition name "{0}" is already defined
ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantDeleteDefaultPartition=Can not delete default partition
ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantRenameDefaultPartition=Can not rename default partition
ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor.unknownTenantName=Unknown tenant: {0}

View File

@ -174,7 +174,7 @@ public class JpaServerDemo extends RestfulServer {
DaoRegistry daoRegistry = myAppCtx.getBean(DaoRegistry.class);
IInterceptorBroadcaster interceptorBroadcaster = myAppCtx.getBean(IInterceptorBroadcaster.class);
CascadingDeleteInterceptor cascadingDeleteInterceptor = new CascadingDeleteInterceptor(daoRegistry, interceptorBroadcaster);
CascadingDeleteInterceptor cascadingDeleteInterceptor = new CascadingDeleteInterceptor(ctx, daoRegistry, interceptorBroadcaster);
getInterceptorService().registerInterceptor(cascadingDeleteInterceptor);
}

View File

@ -89,7 +89,12 @@ public class OkHttpRestfulRequest implements IHttpRequest {
return myUrl;
}
@Override
@Override
public void setUri(String theUrl) {
myUrl = theUrl;
}
@Override
public String getHttpVerbName() {
return myRequestTypeEnum.name();
}

View File

@ -34,6 +34,7 @@ import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.ContentType;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.*;
@ -110,6 +111,11 @@ public class ApacheHttpRequest implements IHttpRequest {
return null;
}
@Override
public void setUri(String theUrl) {
myRequest.setURI(URI.create(theUrl));
}
@Override
public String getUri() {
return myRequest.getURI().toString();

View File

@ -297,6 +297,7 @@ public abstract class BaseClient implements IRestfulClient {
HookParams requestParams = new HookParams();
requestParams.add(IHttpRequest.class, httpRequest);
requestParams.add(IRestfulClient.class, this);
getInterceptorService().callHooks(Pointcut.CLIENT_REQUEST, requestParams);
response = httpRequest.execute();
@ -304,6 +305,7 @@ public abstract class BaseClient implements IRestfulClient {
HookParams responseParams = new HookParams();
responseParams.add(IHttpRequest.class, httpRequest);
responseParams.add(IHttpResponse.class, response);
responseParams.add(IRestfulClient.class, this);
getInterceptorService().callHooks(Pointcut.CLIENT_RESPONSE, responseParams);
String mimeType;

View File

@ -83,7 +83,7 @@ public class ClientInvocationHandlerFactory {
class RegisterInterceptorLambda implements ILambda {
@Override
public Object handle(ClientInvocationHandler theTarget, Object[] theArgs) {
IClientInterceptor interceptor = (IClientInterceptor) theArgs[0];
Object interceptor = theArgs[0];
theTarget.registerInterceptor(interceptor);
return null;
}
@ -130,7 +130,7 @@ public class ClientInvocationHandlerFactory {
class UnregisterInterceptorLambda implements ILambda {
@Override
public Object handle(ClientInvocationHandler theTarget, Object[] theArgs) {
IClientInterceptor interceptor = (IClientInterceptor) theArgs[0];
Object interceptor = theArgs[0];
theTarget.unregisterInterceptor(interceptor);
return null;
}

View File

@ -604,26 +604,41 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
private class DeleteInternal extends BaseSearch<IDeleteTyped, IDeleteWithQueryTyped, IBaseOperationOutcome> implements IDelete, IDeleteTyped, IDeleteWithQuery, IDeleteWithQueryTyped {
private class DeleteInternal extends BaseSearch<IDeleteTyped, IDeleteWithQueryTyped, MethodOutcome> implements IDelete, IDeleteTyped, IDeleteWithQuery, IDeleteWithQueryTyped {
private boolean myConditional;
private IIdType myId;
private String myResourceType;
private String mySearchUrl;
private DeleteCascadeModeEnum myCascadeMode;
@Override
public IBaseOperationOutcome execute() {
public MethodOutcome execute() {
Map<String, List<String>> additionalParams = new HashMap<>();
if (myCascadeMode != null) {
switch (myCascadeMode) {
case DELETE:
addParam(getParamMap(), Constants.PARAMETER_CASCADE_DELETE, Constants.CASCADE_DELETE);
break;
default:
case NONE:
break;
}
}
HttpDeleteClientInvocation invocation;
if (myId != null) {
invocation = DeleteMethodBinding.createDeleteInvocation(getFhirContext(), myId);
invocation = DeleteMethodBinding.createDeleteInvocation(getFhirContext(), myId, getParamMap());
} else if (myConditional) {
invocation = DeleteMethodBinding.createDeleteInvocation(getFhirContext(), myResourceType, getParamMap());
} else {
invocation = DeleteMethodBinding.createDeleteInvocation(getFhirContext(), mySearchUrl);
invocation = DeleteMethodBinding.createDeleteInvocation(getFhirContext(), mySearchUrl, getParamMap());
}
OperationOutcomeResponseHandler binding = new OperationOutcomeResponseHandler();
Map<String, List<String>> params = new HashMap<String, List<String>>();
return invoke(params, binding, invocation);
OutcomeResponseHandler binding = new OutcomeResponseHandler();
return invoke(additionalParams, binding, invocation);
}
@Override
@ -687,6 +702,11 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this;
}
@Override
public IDeleteTyped cascade(DeleteCascadeModeEnum theDelete) {
myCascadeMode = theDelete;
return this;
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
@ -1370,30 +1390,6 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
}
private final class OperationOutcomeResponseHandler implements IClientResponseHandler<IBaseOperationOutcome> {
@Override
public IBaseOperationOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
return null;
}
IParser parser = respType.newParser(myContext);
IBaseOperationOutcome retVal;
try {
// TODO: handle if something else than OO comes back
retVal = (IBaseOperationOutcome) parser.parseResource(theResponseInputStream);
} catch (DataFormatException e) {
ourLog.warn("Failed to parse OperationOutcome response", e);
return null;
}
MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, retVal);
return retVal;
}
}
private final class OutcomeResponseHandler implements IClientResponseHandler<MethodOutcome> {
private PreferReturnEnum myPrefer;

View File

@ -35,7 +35,7 @@ import org.apache.commons.lang3.Validate;
* HTTP interceptor to be used for adding HTTP basic auth username/password tokens
* to requests
* <p>
* See the <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_client_interceptor.html#Security_HTTP_Basic_Authorization">HAPI Documentation</a>
* See the <a href="https://hapifhir.io/hapi-fhir/docs/interceptors/built_in_client_interceptors.html">HAPI Documentation</a>
* for information on how to use this class.
* </p>
*/

View File

@ -35,7 +35,7 @@ import ca.uhn.fhir.util.CoverageIgnore;
* where the token portion (at the end of the header) is supplied by the invoking code.
* </p>
* <p>
* See the <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_client_interceptor.html#Security_HTTP_Bearer_Token_Authorization">HAPI Documentation</a> for information on how to use this class.
* See the <a href="https://hapifhir.io/hapi-fhir/docs/interceptors/built_in_client_interceptors.html">HAPI Documentation</a> for information on how to use this class.
* </p>
*/
public class BearerTokenAuthInterceptor implements IClientInterceptor {

View File

@ -20,7 +20,9 @@ package ca.uhn.fhir.rest.client.interceptor;
* #L%
*/
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
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.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -35,7 +37,8 @@ import java.io.IOException;
*
* @see ThreadLocalCapturingInterceptor for an interceptor that uses a ThreadLocal in order to work in multithreaded environments
*/
public class CapturingInterceptor implements IClientInterceptor {
@Interceptor
public class CapturingInterceptor {
private IHttpRequest myLastRequest;
private IHttpResponse myLastResponse;
@ -56,12 +59,12 @@ public class CapturingInterceptor implements IClientInterceptor {
return myLastResponse;
}
@Override
@Hook(value = Pointcut.CLIENT_REQUEST, order = InterceptorOrders.CAPTURING_INTERCEPTOR_REQUEST)
public void interceptRequest(IHttpRequest theRequest) {
myLastRequest = theRequest;
}
@Override
@Hook(value = Pointcut.CLIENT_RESPONSE, order = InterceptorOrders.CAPTURING_INTERCEPTOR_RESPONSE)
public void interceptResponse(IHttpResponse theResponse) {
//Buffer the reponse to avoid errors when content has already been read and the entity is not repeatable
bufferResponse(theResponse);

View File

@ -0,0 +1,31 @@
package ca.uhn.fhir.rest.client.interceptor;
/*-
* #%L
* HAPI FHIR - Client Framework
* %%
* 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%
*/
public interface InterceptorOrders {
int LOGGING_INTERCEPTOR_REQUEST = -2;
int URL_TENANT_SELECTION_INTERCEPTOR_REQUEST = 100;
int CAPTURING_INTERCEPTOR_REQUEST = 1000;
int CAPTURING_INTERCEPTOR_RESPONSE = -1;
int LOGGING_INTERCEPTOR_RESPONSE = 1001;
}

View File

@ -20,6 +20,19 @@ package ca.uhn.fhir.rest.client.interceptor;
* #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.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@ -27,20 +40,6 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@Interceptor
public class LoggingInterceptor implements IClientInterceptor {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LoggingInterceptor.class);
@ -62,9 +61,8 @@ public class LoggingInterceptor implements IClientInterceptor {
/**
* Constructor for client logging interceptor
*
* @param theVerbose
* If set to true, all logging is enabled
*
* @param theVerbose If set to true, all logging is enabled
*/
public LoggingInterceptor(boolean theVerbose) {
if (theVerbose) {
@ -78,7 +76,7 @@ public class LoggingInterceptor implements IClientInterceptor {
}
@Override
@Hook(Pointcut.CLIENT_REQUEST)
@Hook(value = Pointcut.CLIENT_REQUEST, order = InterceptorOrders.LOGGING_INTERCEPTOR_RESPONSE)
public void interceptRequest(IHttpRequest theRequest) {
if (myLogRequestSummary) {
myLog.info("Client request: {}", theRequest);
@ -102,7 +100,7 @@ public class LoggingInterceptor implements IClientInterceptor {
}
@Override
@Hook(Pointcut.CLIENT_RESPONSE)
@Hook(value = Pointcut.CLIENT_RESPONSE, order = InterceptorOrders.LOGGING_INTERCEPTOR_REQUEST)
public void interceptResponse(IHttpResponse theResponse) throws IOException {
if (myLogResponseSummary) {
String message = "HTTP " + theResponse.getStatus() + " " + theResponse.getStatusInfo();
@ -167,18 +165,18 @@ public class LoggingInterceptor implements IClientInterceptor {
StringBuilder b = new StringBuilder();
if (theHeaders != null && !theHeaders.isEmpty()) {
Iterator<String> nameEntries = theHeaders.keySet().iterator();
while(nameEntries.hasNext()) {
while (nameEntries.hasNext()) {
String key = nameEntries.next();
Iterator<String> values = theHeaders.get(key).iterator();
while(values.hasNext()) {
while (values.hasNext()) {
String value = values.next();
b.append(key);
b.append(": ");
b.append(value);
if (nameEntries.hasNext() || values.hasNext()) {
b.append('\n');
}
b.append(key);
b.append(": ");
b.append(value);
if (nameEntries.hasNext() || values.hasNext()) {
b.append('\n');
}
}
}
}
return b;
@ -187,9 +185,8 @@ public class LoggingInterceptor implements IClientInterceptor {
/**
* Sets a logger to use to log messages (default is a logger with this class' name). This can be used to redirect
* logs to a differently named logger instead.
*
* @param theLogger
* The logger to use. Must not be null.
*
* @param theLogger The logger to use. Must not be null.
*/
public void setLogger(Logger theLogger) {
Validate.notNull(theLogger, "theLogger can not be null");

View File

@ -0,0 +1,88 @@
package ca.uhn.fhir.rest.client.interceptor;
/*-
* #%L
* HAPI FHIR - Client Framework
* %%
* 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.Pointcut;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IRestfulClient;
import org.apache.commons.lang3.Validate;
import static org.apache.commons.lang3.StringUtils.isBlank;
/**
* This interceptor adds a path element representing the tenant ID to each client request. It is primarily
* intended to be used with clients that are accessing servers using
* <a href="https://hapifhir.io/hapi-fhir/docs/server_plain/multitenancy.html#url-base-multitenancy">URL Base Multitenancy</a>.
*/
public class UrlTenantSelectionInterceptor {
private String myTenantId;
/**
* Constructor
*/
public UrlTenantSelectionInterceptor() {
this(null);
}
/**
* Constructor
*
* @param theTenantId The tenant ID to add to URL base
*/
public UrlTenantSelectionInterceptor(String theTenantId) {
myTenantId = theTenantId;
}
/**
* Returns the tenant ID
*/
public String getTenantId() {
return myTenantId;
}
/**
* Sets the tenant ID
*/
public void setTenantId(String theTenantId) {
myTenantId = theTenantId;
}
@Hook(value = Pointcut.CLIENT_REQUEST, order = InterceptorOrders.URL_TENANT_SELECTION_INTERCEPTOR_REQUEST)
public void request(IRestfulClient theClient, IHttpRequest theRequest) {
String tenantId = getTenantId();
if (isBlank(tenantId)) {
return;
}
String requestUri = theRequest.getUri();
String serverBase = theClient.getServerBase();
if (serverBase.endsWith("/")) {
serverBase = serverBase.substring(0, serverBase.length() - 1);
}
Validate.isTrue(requestUri.startsWith(serverBase), "Request URI %s does not start with server base %s", requestUri, serverBase);
String newUri = serverBase + "/" + tenantId + requestUri.substring(serverBase.length());
theRequest.setUri(newUri);
}
}

View File

@ -65,18 +65,18 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBindingWithRe
@Override
public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
IIdType idDt = (IIdType) theArgs[getIdParameterIndex()];
if (idDt == null) {
IIdType id = (IIdType) theArgs[getIdParameterIndex()];
if (id == null) {
throw new NullPointerException("ID can not be null");
}
if (idDt.hasResourceType() == false) {
idDt = idDt.withResourceType(getResourceName());
} else if (getResourceName().equals(idDt.getResourceType()) == false) {
throw new InvalidRequestException("ID parameter has the wrong resource type, expected '" + getResourceName() + "', found: " + idDt.getResourceType());
if (id.hasResourceType() == false) {
id = id.withResourceType(getResourceName());
} else if (getResourceName().equals(id.getResourceType()) == false) {
throw new InvalidRequestException("ID parameter has the wrong resource type, expected '" + getResourceName() + "', found: " + id.getResourceType());
}
HttpDeleteClientInvocation retVal = createDeleteInvocation(getContext(), idDt);
HttpDeleteClientInvocation retVal = createDeleteInvocation(getContext(), id, Collections.emptyMap());
for (int idx = 0; idx < theArgs.length; idx++) {
IParameter nextParam = getParameters().get(idx);
@ -86,9 +86,8 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBindingWithRe
return retVal;
}
public static HttpDeleteClientInvocation createDeleteInvocation(FhirContext theContext, IIdType theId) {
HttpDeleteClientInvocation retVal = new HttpDeleteClientInvocation(theContext, theId);
return retVal;
public static HttpDeleteClientInvocation createDeleteInvocation(FhirContext theContext, IIdType theId, Map<String, List<String>> theAdditionalParams) {
return new HttpDeleteClientInvocation(theContext, theId, theAdditionalParams);
}
@ -97,13 +96,8 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBindingWithRe
return null;
}
public static HttpDeleteClientInvocation createDeleteInvocation(FhirContext theContext, String theSearchUrl) {
HttpDeleteClientInvocation retVal = new HttpDeleteClientInvocation(theContext, theSearchUrl);
return retVal;
}
public static HttpDeleteClientInvocation createDeleteInvocation(FhirContext theContext, String theResourceType, Map<String, List<String>> theParams) {
return new HttpDeleteClientInvocation(theContext, theResourceType, theParams);
public static HttpDeleteClientInvocation createDeleteInvocation(FhirContext theContext, String theSearchUrl, Map<String, List<String>> theParams) {
return new HttpDeleteClientInvocation(theContext, theSearchUrl, theParams);
}
}

View File

@ -36,19 +36,15 @@ public class HttpDeleteClientInvocation extends BaseHttpClientInvocation {
private String myUrlPath;
private Map<String, List<String>> myParams;
public HttpDeleteClientInvocation(FhirContext theContext, IIdType theId) {
public HttpDeleteClientInvocation(FhirContext theContext, IIdType theId, Map<String, List<String>> theAdditionalParams) {
super(theContext);
myUrlPath = theId.toUnqualifiedVersionless().getValue();
myParams = theAdditionalParams;
}
public HttpDeleteClientInvocation(FhirContext theContext, String theSearchUrl) {
public HttpDeleteClientInvocation(FhirContext theContext, String theSearchUrl, Map<String, List<String>> theParams) {
super(theContext);
myUrlPath = theSearchUrl;
}
public HttpDeleteClientInvocation(FhirContext theContext, String theResourceType, Map<String, List<String>> theParams) {
super(theContext);
myUrlPath = theResourceType;
myParams = theParams;
}
@ -67,10 +63,4 @@ public class HttpDeleteClientInvocation extends BaseHttpClientInvocation {
return createHttpRequest(b.toString(), theEncoding, RequestTypeEnum.DELETE);
}
@Override
protected IHttpRequest createHttpRequest(String theUrl, EncodingEnum theEncoding, RequestTypeEnum theRequestType) {
// TODO Auto-generated method stub
return super.createHttpRequest(theUrl, theEncoding, theRequestType);
}
}

View File

@ -67,6 +67,12 @@
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r5</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-validation-resources-dstu2</artifactId>

View File

@ -23,6 +23,7 @@ package ca.uhn.hapi.fhir.docs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.PerformanceOptionsEnum;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.SearchStyleEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
@ -235,21 +236,27 @@ public class GenericClientExample {
// START SNIPPET: conformance
// Retrieve the server's conformance statement and print its
// description
CapabilityStatement conf = client.capabilities().ofType(CapabilityStatement.class).execute();
CapabilityStatement conf = client
.capabilities()
.ofType(CapabilityStatement.class)
.execute();
System.out.println(conf.getDescriptionElement().getValue());
// END SNIPPET: conformance
}
{
// START SNIPPET: delete
IBaseOperationOutcome resp = client.delete().resourceById(new IdType("Patient", "1234")).execute();
MethodOutcome response = client
.delete()
.resourceById(new IdType("Patient", "1234"))
.execute();
// outcome may be null if the server didn't return one
if (resp != null) {
OperationOutcome outcome = (OperationOutcome) resp;
System.out.println(outcome.getIssueFirstRep().getDetails().getCodingFirstRep().getCode());
}
// END SNIPPET: delete
}
OperationOutcome outcome = (OperationOutcome) response.getOperationOutcome();
if (outcome != null) {
System.out.println(outcome.getIssueFirstRep().getDetails().getCodingFirstRep().getCode());
}
// END SNIPPET: delete
}
{
// START SNIPPET: deleteConditional
client.delete()
@ -262,6 +269,19 @@ public class GenericClientExample {
.execute();
// END SNIPPET: deleteConditional
}
{
// START SNIPPET: deleteCascade
client.delete()
.resourceById(new IdType("Patient/123"))
.cascade(DeleteCascadeModeEnum.DELETE)
.execute();
client.delete()
.resourceConditionalByType("Patient")
.where(Patient.IDENTIFIER.exactly().systemAndIdentifier("system", "00001"))
.execute();
// END SNIPPET: deleteCascade
}
{
// START SNIPPET: search
Bundle response = client.search()
@ -342,7 +362,8 @@ public class GenericClientExample {
.revInclude(Provenance.INCLUDE_TARGET)
.lastUpdated(new DateRangeParam("2011-01-01", null))
.sort().ascending(Patient.BIRTHDATE)
.sort().descending(Patient.NAME).limitTo(123)
.sort().descending(Patient.NAME)
.count(123)
.returnBundle(Bundle.class)
.execute();
// END SNIPPET: searchAdv

View File

@ -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.RequestPartitionId;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
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 RequestPartitionId PartitionIdentifyCreate(ServletRequestDetails theRequestDetails) {
return extractPartitionIdFromRequest(theRequestDetails);
}
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)
public RequestPartitionId PartitionIdentifyRead(ServletRequestDetails theRequestDetails) {
return extractPartitionIdFromRequest(theRequestDetails);
}
private RequestPartitionId extractPartitionIdFromRequest(ServletRequestDetails theRequestDetails) {
// We will use the tenant ID that came from the request as the partition name
String tenantId = theRequestDetails.getTenantId();
return RequestPartitionId.fromPartitionName(tenantId);
}
}
// END SNIPPET: partitionInterceptorRequestPartition
// START SNIPPET: partitionInterceptorHeaders
@Interceptor
public class CustomHeaderBasedPartitionInterceptor {
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
public RequestPartitionId PartitionIdentifyCreate(ServletRequestDetails theRequestDetails) {
String partitionName = theRequestDetails.getHeader("X-Partition-Name");
return RequestPartitionId.fromPartitionName(partitionName);
}
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)
public RequestPartitionId PartitionIdentifyRead(ServletRequestDetails theRequestDetails) {
String partitionName = theRequestDetails.getHeader("X-Partition-Name");
return RequestPartitionId.fromPartitionName(partitionName);
}
}
// END SNIPPET: partitionInterceptorHeaders
// START SNIPPET: partitionInterceptorResourceContents
@Interceptor
public class ResourceTypePartitionInterceptor {
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
public RequestPartitionId PartitionIdentifyCreate(IBaseResource theResource) {
if (theResource instanceof Patient) {
return RequestPartitionId.fromPartitionName("PATIENT");
} else if (theResource instanceof Observation) {
return RequestPartitionId.fromPartitionName("OBSERVATION");
} else {
return RequestPartitionId.fromPartitionName("OTHER");
}
}
}
// END SNIPPET: partitionInterceptorResourceContents
// START SNIPPET: multitenantServer
public class MultitenantServer extends RestfulServer {
@Autowired
private PartitionSettings myPartitionSettings;
@Override
protected void initialize() {
// Enable partitioning
myPartitionSettings.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
}

View File

@ -20,6 +20,7 @@
"contactName": "Kevin Mayfield",
"contactEmail": "Kevin.mayfield@mayfield-is.co.uk",
"link": "https://data.developer.nhs.uk/ccri/exp",
"city": "UK",
"lat": 55.378052,
"lon": -3.435973,
"added": "2019-08-19"
@ -29,6 +30,7 @@
"contactName": "David Hay",
"contactEmail": "david.hay25@gmail.com",
"link": "http://clinfhir.com",
"city": "New Zealand",
"lat": -42.651737,
"lon": 171.926909,
"added": "2019-08-18"

View File

@ -0,0 +1,10 @@
---
type: change
issue: 1698
title: "Removed the SEARCH_LAST_RETURNED column of the HFJ_SEARCH table. HAPI FHIR updated the HFJ_SEARCH table with every
request, but this led to unecessary database load. The purpose of this column was to ensure that search results were
kept around long enough for systems that needed them for paging (default one hour). When expiring search results,
we used to add one hour to SEARCH_LAST_RETURNED to determine the expiry time. However, the length of time where a search
result could be updated was relatively small (default one minute). So rather than keeping track of the expiry time to expire
exactly one hour after the last returned time, hapi now simply expires after the maximum possible length of time (default one hour
plus one minute). This eliminates the need to update the HFJ_SEARCH table with every search."

View File

@ -0,0 +1,6 @@
---
type: add
issue: 1783
title: "The client interceptor pointcuts `CLIENT_REQUEST` and `CLIENT_RESPONSE` now allow a parameter of type `IRestfulClient` to be
injected, containing a reference to the client making the request. In addition, CLIENT_REQUEST interceptors are now able to
modify the URL of the request before it is performed."

View File

@ -0,0 +1,6 @@
---
type: add
issue: 1783
title: "A new client interceptor called UrlTenantSelectionInterceptor has been added. This interceptor allows the dynamic
selection of a tenant ID on servers that are using URL Base Tenant Selection."

View File

@ -0,0 +1,8 @@
---
type: add
issue: 1783
title: "In the JPA server, the ModelConfig setting 'DefaultSearchParamsCanBeOverridden' now has a default value of
*true* (previously this was *false*). In addition, when creating/updating a SearchParameter resource, the system
will now raise an error if the client is attempting to override a built-in SearchParameter when this setting
is disabled (previously this was silently ignored)."

View File

@ -0,0 +1,7 @@
---
type: add
issue: 1783
title: "In the JPA sevrer, if overriding built-in search parameters is not enabled, the server
will now return an error if a client tries to create a SearchParameter that is
trying to override one. Previously, the SearchParameter would be stored but silently ignored,
which was confusing."

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 1791
title: The GraphQL Expression parser sometimes fails and reports unhelpful error messages when using search arguments.
Thanks to Ibrohim Kholilul Islam for the pull request!

View File

@ -0,0 +1,5 @@
---
type: add
issue: 1798
title: New mthods have been added to DateClientParam allowing searching at MILLIS precision. Thanks to
David Gileadi for the pull request!

View File

@ -2,5 +2,6 @@
type: fix
issue: 1801
title: "In previous versions of HAPI FHIR, the server incorrectly silently accepted decimal numbers in JSON with no
leading numbers (e.g. `.123`). These will now be rejected by the JSON parser. Any values with this string that
have previously been stored in the JPA server database will now automatically convert the value to `0.123`."
leading numbers (e.g. `.123`), as well as decimal numbers with more than one leading zero (e.g. 00.123). These will
now be rejected by the JSON parser. Any values with this string that have previously been stored in the JPA server
database will now automatically normalize the value to `0.123`."

View File

@ -0,0 +1,4 @@
---
type: add
issue: 1804
title: Support for HAPI FHIR cascading deletes has been added to the Generic Client.

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 1806
title: The JPA server ElasticSearch provider failed to initialize if username/password credentials were not
explicitly provided, meaning it could not run on AWS-supplied ElasticSearch. Thanks to Maciej Kucharek for
the pull request!

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 1810
title: The text styling on the Testpage Overlay homepage has been improved to use native
Bootstrap warning colours. Thanks to Joel Schneider for the pull request!

View File

@ -4,8 +4,11 @@
title: "The version of a few dependencies have been bumped to the latest versions
(dependent HAPI modules listed in brackets):
<ul>
<li>Hibernate ORM (JPA): 5.4.6 -&gt; 5.4.12</li>
<li>Hibernate ORM (JPA): 5.4.6 -&gt; 5.4.14</li>
<li>Hibernate Search (JPA): 5.11.3 -&gt; 5.11.5</li>
<li>Hibernate Validator (JPA): 5.4.2.Final -&gt; 6.1.3.Final</li>
<li>Guava (JPA): 28.0 -&gt; 28.2</li>
<li>Spring Boot (Boot): 2.2.0.RELEASE -&gt; 2.2.6.RELEASE</li>
</ul>"
- item:
issue: "1583"
@ -33,3 +36,28 @@
These classes have not changed in terms of functionality, but existing projects may need to adjust some
package import statements.
"
- item:
issue: "1804"
type: "change"
title: "**Breaking Change**:
The Generic/Fluent **delete()** operation now returns a [MethodOutcome](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/rest/api/MethodOutcome.html)
object instead of an OperationOutcome. The OperationOutcomoe is still available direcly by querying
the MethodOutcome object, but this change makes the delete() method more consistent with
other similar methods in the API.
"
- item:
type: "change"
title: "**Breaking Change**:
Some R4 and R5 structure fields containing a `code` value with a **Required (closed) binding**
did not use the java Enum type that was generated for the given field. These have been changed
to use the Enum values where possible. This change does not remove any functionality from the model
but may require a small amount of re-coding to deal with new setter/getter types on a few fields.
"
- item:
issue: "1807"
type: "change"
title: "**New Feature**:
A new feature has been added to the JPA server called **[Partitioning](/hapi-fhir/docs/server_jpa/partitioning.html). This
feature allows data to be segregated using a user defined partitioning strategy. This can be leveraged to take
advantags of native RDBMS partition strategies, and also to implement **multitenant servers**.
"

View File

@ -8,7 +8,9 @@ See the [Modules Page](/docs/introduction/modules.html) for more information on
* [Model API (R4)](/apidocs/hapi-fhir-structures-r4/) - hapi-fhir-structures-r4
* [Model API (R5)](/apidocs/hapi-fhir-structures-r5/) - hapi-fhir-structures-r5
* [Client API](/apidocs/hapi-fhir-client/) - hapi-fhir-client
* [Server API (Plain)](/apidocs/hapi-fhir-server/) - hapi-fhir-server
* [Server API (JPA)](/apidocs/hapi-fhir-jpaserver-base/) - hapi-fhir-jpaserver-base
* [Plain Server API](/apidocs/hapi-fhir-server/) - hapi-fhir-server
* [JPA Server - API](/apidocs/hapi-fhir-jpaserver-api/) - hapi-fhir-jpaserver-api
* [JPA Server - Model](/apidocs/hapi-fhir-jpaserver-model/) - hapi-fhir-jpaserver-model
* [JPA Server - Base](/apidocs/hapi-fhir-jpaserver-base/) - hapi-fhir-jpaserver-base
* [Version Converter API](/apidocs/hapi-fhir-converter/) - hapi-fhir-converter
* [Server API (JAX-RS)](/apidocs/hapi-fhir-jaxrsserver-base/) - hapi-fhir-jaxrsserver-base

View File

@ -181,6 +181,14 @@ Conditional deletions are also possible, which is a form where instead of deleti
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/GenericClientExample.java|deleteConditional}}
```
## Cascading Delete
The following snippet shows now to request a cascading delete. Note that this is a HAPI FHIR specific feature and is not supported on all servers.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/GenericClientExample.java|deleteCascade}}
```
# Update - Instance
Updating a resource is similar to creating one, except that an ID must be supplied since you are updating a previously existing resource instance.

View File

@ -41,9 +41,11 @@ section.server_jpa.title=JPA Server
page.server_jpa.introduction=Introduction
page.server_jpa.get_started=Get Started ⚡
page.server_jpa.architecture=Architecture
page.server_jpa.schema=Database Schema
page.server_jpa.configuration=Configuration
page.server_jpa.search=Search
page.server_jpa.performance=Performance
page.server_jpa.partitioning=Partitioning and Multitenancy
page.server_jpa.upgrading=Upgrade Guide
section.interceptors.title=Interceptors

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -75,6 +75,14 @@ The following example shows how to configure your client to inject a bearer toke
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ClientExamples.java|cookie}}
```
# Multitenancy: Add tenant ID to path
When communicating with a server that supports [URL Base Multitenancy](/docs/server_plain/multitenancy.html#url-base-multitenancy), an extra element needs to be added to the request path. This can be done by simply appending the path to the base URL supplied to the client, but it can also be dynamically appended using this interceptor.
* [UrlTenantSelectionInterceptor JavaDoc](/apidocs/hapi-fhir-client/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptor.html)
* [UrlTenantSelectionInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptor.java)
# Performance: GZip Outgoing Request Bodies
The GZipContentInterceptor compresses outgoing contents. With this interceptor, if the client is transmitting resources to the server (e.g. for a create, update, transaction, etc.) the content will be GZipped before transmission to the server.

View File

@ -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[]
```
<a name="request-tenant-partition-interceptor"/>
# 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.
@ -146,6 +156,13 @@ Some security audit tools require that servers return an HTTP 405 if an unsuppor
* [BanUnsupportedHttpMethodsInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/BanUnsupportedHttpMethodsInterceptor.html)
* [BanUnsupportedHttpMethodsInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/BanUnsupportedHttpMethodsInterceptor.java)
# Subscription: Subscription Debug Log Interceptor
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-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

View File

@ -0,0 +1,332 @@
# Partitioning and Multitenancy
HAPI FHIR 5.0.0 introduced a new feature to HAPI FHIR JPA server called **Partitioning**.
Partitioning allows each resource on the server to be placed in a partition, which is essentially just an arbitrary identifier grouping a set of resources together.
Partitioning is designed to be very flexible, and can be used to achieve different outcomes. For example:
* Partitioning could be used to achieve **multitenancy**, where there are multiple logically separate pools of resources on the server. Traditionally this kind of setup is desired when each of these pools belongs to a distinct user group / organization / customer / etc. (a "tenant"), and each of these tenants should not be able to access or modify data belonging to another tenant.
* Partitioning could also be used to **logically separate data coming from distinct sources** within an organization. For example, patient records might be placed in one partition, lab data sourced from a lab system might be placed in a second partition and patient surveys from a survey app might be placed in another. In this situation data does not need to be completely segregated (lab Observation records may have references to Patient records in the patient partition) but these partitions might be used to support security groups, retention policies, etc.
* Partitioning could be used for **geographic sharding**, keeping data in a partition that is geographically closest to where it is likely to be used.
These examples each have different properties in terms of security rules, and how data is organized and searched.
# Architecture
## Conceptual Architecture
Partitioning in HAPI FHIR JPA means that every resource has a partition identity. This identity consists of the following attributes:
* **Partition Name**: This is a short textual identifier for the partition that the resource belongs to. This might be a customer ID, a description of the type of data in the partition, or something else. There is no restriction on the text used aside from a maximum length of 200, but generally it makes sense to limit the text to URL-friendly characters.
* **Partition ID**: This is an integer ID that corresponds 1:1 with the partition Name. It is used in the database as the partition identifier.
* **Partition Date**: This is an additional partition discriminator that can be used to implement partitioning strategies using a date axis.
Mappings between the **Partition Name** and the **Partition ID** are maintained using the [Partition Mapping Operations](#partition-mapping-operations).
## Logical Architecture
At the database level, partitioning involves the use of two dedicated columns to many tables within the HAPI FHIR JPA [database schema](./schema.html):
* **PARTITION_ID** &ndash; 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** &ndash; 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 for a given resource 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.)
When a new resource is **created**, an [interceptor hook](#partition-interceptors) is invoked to request the partition ID and date to be assigned to the resource.
When a resource is **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-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:
* The system can be configured to operate as a **multitenant** solution by configuring the partition interceptor to scope all read operations to read data only from the partition that request has access to.```
* The system can be configured to operate with logical segments by configuring the partition interceptor to scope read operations to access all partitions.
# Enabling Partitioning in HAPI FHIR
Follow these steps to enable partitioning on the server:
The [PartitionSettings](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html) bean contains configuration settings related to partitioning within the server. To enable partitioning, the [setPartitioningEnabled(boolean)](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html#setPartitioningEnabled(boolean)) property should be enabled.
The following settings can be enabled:
* **Include Partition in Search Hashes** ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.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](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html#setAllowReferencesAcrossPartitions(ca.uhn.fhir.jpa.model.config.PartitionSettings.CrossPartitionReferenceMode))): This setting controls whether resources in one partition should be allowed to create references to resources in other partitions.
# Partition Interceptors
In order to implement partitioning, an interceptor must be registered against the interceptor registry (either the REST Server registry, or the JPA Server registry will work).
This interceptor can implement the hooks shown below.
## Identify Partition for Create (Required)
A hook against the [`Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE`](/hapi-fhir/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 created in order to determine the partition the resource is assigned to.
The criteria for determining the partition will depend on your use case. For example:
* 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.
## Identify Partition for Read (Optional)
A hook against the [`Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE`](/hapi-fhir/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 created in order to determine the partition to assign the resource to.
## 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|partitionInterceptorRequestPartition}}
```
## 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}}
```
<a name="partition-mapping-operations"/>
# Partition Mapping Operations
Several operations exist that can be used to manage the existence of partitions. These operations are supplied by a [plain provider](/docs/server_plain/resource_providers.html#plain-providers) called [PartitionManagementProvider](/hapi-fhir/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.html).
Before a partition can be used, it must be registered using these methods.
## Creating a Partition
The `$partition-management-add-partition` operation can be used to create a new partition. This operation takes the following parameters:
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Cardinality</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>id</td>
<td>Integer</td>
<td>1..1</td>
<td>
The numeric ID for the partition. This value can be any integer, positive or negative or zero. It must not be a value that has already been used.
</td>
</tr>
<tr>
<td>name</td>
<td>Code</td>
<td>1..1</td>
<td>
A code (string) to assign to the partition.
</td>
</tr>
<tr>
<td>description</td>
<td>String</td>
<td>0..1</td>
<td>
An optional description for the partition.
</td>
</tr>
</tbody>
</table>
### Example
An HTTP POST to the following URL would be used to invoke this operation:
```url
http://example.com/$partition-management-add-partition
```
The following request body could be used:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "id",
"valueInteger": 123
}, {
"name": "name",
"valueCode": "PARTITION-123"
}, {
"name": "description",
"valueString": "a description"
} ]
}
```
## Updating a Partition
The `$partition-management-update-partition` operation can be used to update an existing partition. This operation takes the following parameters:
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Cardinality</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>id</td>
<td>Integer</td>
<td>1..1</td>
<td>
The numeric ID for the partition to update. This ID must already exist.
</td>
</tr>
<tr>
<td>name</td>
<td>Code</td>
<td>1..1</td>
<td>
A code (string) to assign to the partition. Note that it is acceptable to change the name of a partition, but this should be done with caution since partition names may be referenced by URLs, caches, etc.
</td>
</tr>
<tr>
<td>description</td>
<td>String</td>
<td>0..1</td>
<td>
An optional description for the partition.
</td>
</tr>
</tbody>
</table>
### Example
An HTTP POST to the following URL would be used to invoke this operation:
```url
http://example.com/$partition-management-add-partition
```
The following request body could be used:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "id",
"valueInteger": 123
}, {
"name": "name",
"valueCode": "PARTITION-123"
}, {
"name": "description",
"valueString": "a description"
} ]
}
```
## Deleting a Partition
The `$partition-management-delete-partition` operation can be used to delete an existing partition. This operation takes the following parameters:
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Cardinality</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>id</td>
<td>Integer</td>
<td>1..1</td>
<td>
The numeric ID for the partition to update. This ID must already exist.
</td>
</tr>
</tbody>
</table>
### Example
An HTTP POST to the following URL would be used to invoke this operation:
```url
http://example.com/$partition-management-delete-partition
```
The following request body could be used:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "id",
"valueInteger": 123
} ]
}
```
# Limitations
Partitioning is a relatively new feature in HAPI FHIR (added in HAPI FHIR 5.0.0) and has a number of known limitations. If you are intending to use partitioning for achieving a multi-tenant architecture it is important to consider these limitations.
None of the limitations listed here are considered permanent. Over time the HAPI FHIR team is hoping to make all of these features partition aware.
* **Server Capability Statement is not partition aware**: The server creates and exposes a single server capability statement, covering all partitions. This can be misleading when partitioning us used as a multitenancy strategy.
* **Subscriptions may not be partitioned**: All subscriptions must be placed in the default partition, and subscribers will receive deliveries for any matching resources from all partitions.
* **Conformance resources may not be partitioned**: The following resources must be placed in the default partition, and will be shared for any validation activities across all partitions:
* StructureDefinition
* Questionnaire
* ValueSet
* CodeSystem
* ConceptMap
* **Search Parameters are not partitioned**: There is only one set of SearchParameter resources for the entire system, and any search parameters will apply to resources in all partitions. All SearchParameter resources must be stored in the default partition.
* **Bulk Operations are not partition aware**: Bulk export operations will export data across all partitions.

View File

@ -0,0 +1,524 @@
# HAPI FHIR JPA Schema
**This page is a work in progress. It is not yet comprehensive.**
It contains a description of the tables within the HAPI FHIR JPA database. Note that columns are shown using Java datatypes as opposed to SQL datatypes, because the exact SQL datatype used will vary depending on the underlying database platform. The schema creation scripts can be used to determine the underlying column types.
# Background: Persistent IDs (PIDs)
The HAPI FHIR JPA schema relies heavily on the concept of internal persistent IDs on tables, using a Java type of Long (8-byte integer, which translates to an *int8* or *number(19)* on various database platforms).
Many tables use an internal persistent ID as their primary key, allowing the flexibility for other more complex business identifiers to be changed and minimizing the amount of data consumed by foreign key relationships. These persistent ID columns are generally assigned using a dedicated database sequence on platforms which support sequences.
The persistent ID column is generally called `PID` in the database schema, although there are exceptions.
<a name="HFJ_RESOURCE"/>
# HFJ_RESOURCE: Resource Master Table
<img src="/hapi-fhir/docs/images/jpa_erd_resources.svg" alt="Resources" style="width: 100%; max-width: 600px;"/>
The HFJ_RESOURCE table indicates a single resource of any type in the database. For example, the resource `Patient/1` will have exactly one row in this table, representing all versions of the resource.
## Columns
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Relationships</th>
<th>Datatype</th>
<th>Nullable</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>PARTITION_ID</td>
<td></td>
<td>Integer</td>
<td>Nullable</td>
<td>
This is the optional partition ID, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
</td>
</tr>
<tr>
<td>PARTITION_DATE</td>
<td></td>
<td>Timestamp</td>
<td>Nullable</td>
<td>
This is the optional partition date, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
</td>
</tr>
<tr>
<td>RES_VER</td>
<td></td>
<td>Long</td>
<td></td>
<td>
This is the current version ID of the resource. Will contain <code>1</code> when the resource is first
created, <code>2</code> the first time it is updated, etc.
This column is equivalent to the <b>HFJ_RES_VER.RES_VER</b>
column, although it does not have a foreign-key dependency in order to allow selective expunge of versions
when necessary. Not to be confused with <b>RES_VERSION</b> below.
</td>
</tr>
<tr>
<td>RES_VERSION</td>
<td></td>
<td>String</td>
<td></td>
<td>
This column contains the FHIR version associated with this resource, using a constant drawn
from <a href="/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/context/FhirVersionEnum.html">FhirVersionEnum</a>.
Not to be confused with <b>RES_VER</b> above.
</td>
</tr>
<tr>
<td>RES_TYPE</td>
<td></td>
<td>String</td>
<td></td>
<td>
Contains the resource type (e.g. <code>Patient</code>)
</td>
</tr>
<tr>
<td>HASH_SHA256</td>
<td></td>
<td>Long</td>
<td></td>
<td>
This column contains a SHA-256 hash of the current resource contents, exclusive of resource metadata.
This is used in order to detect NO-OP writes to the resource.
</td>
</tr>
<tr>
<td>RES_PUBLISHED</td>
<td></td>
<td>Timestamp</td>
<td></td>
<td>
Contains the date that the first version of the resource was created.
</td>
</tr>
<tr>
<td>RES_UPDATED</td>
<td></td>
<td>Timestamp</td>
<td></td>
<td>
Contains the date that the most recent version of the resource was created.
</td>
</tr>
<tr>
<td>RES_DELETED_AT</td>
<td></td>
<td>Timestamp</td>
<td>Nullable</td>
<td>
If the most recent version of the resource is a delete, this contains the timestamp at which
the resource was deleted. Otherwise, contains <i>NULL</i>.
</td>
</tr>
</tbody>
</table>
<a name="HFJ_RES_VER"/>
# HFJ_RES_VER: Resource Versions and Contents
The HFJ_RES_VER table contains individual versions of a resource. If the resource `Patient/1` has 3 versions, there will be 3 rows in this table.
The complete raw contents of the resource is stored in the `RES_TEXT` column, using the encoding specified in the `RES_ENCODING` column.
## Columns
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Relationships</th>
<th>Datatype</th>
<th>Nullable</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>PARTITION_ID</td>
<td></td>
<td>Integer</td>
<td>Nullable</td>
<td>
This is the optional partition ID, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
</td>
</tr>
<tr>
<td>PARTITION_DATE</td>
<td></td>
<td>Timestamp</td>
<td>Nullable</td>
<td>
This is the optional partition date, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
</td>
</tr>
<tr>
<td>PID</td>
<td>PK</td>
<td>Long</td>
<td></td>
<td>
This is the row persistent ID.
</td>
</tr>
<tr>
<td>RES_ID</td>
<td>FK to <a href="#HFJ_RESOURCE">HFJ_RESOURCE</a></td>
<td>Long</td>
<td></td>
<td>
This is the persistent ID of the resource being versioned.
</td>
</tr>
<tr>
<td>RES_VER</td>
<td></td>
<td>Long</td>
<td></td>
<td>
Contains the specific version (starting with 1) of the resource that this row corresponds to.
</td>
</tr>
<tr>
<td>RES_ENCODING</td>
<td></td>
<td>String</td>
<td></td>
<td>
Describes the encoding of the resource being used to store this resource in <b>RES_TEXT</b>. See
<i>Encodings</i> below for allowable values.
</td>
</tr>
<tr>
<td>RES_TEXT</td>
<td></td>
<td>byte[]</td>
<td></td>
<td>
Contains the actual full text of the resource being stored.
</td>
</tr>
</tbody>
</table>
## Encodings
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>JSONC</td>
<td>
The resource is serialized using FHIR JSON encoding, and then compressed into a byte stream using GZIP compression.
</td>
</tr>
</tbody>
</table>
<a name="HFJ_FORCED_ID"/>
# HFJ_FORCED_ID: Client Assigned/Visible Resource IDs
By default, the **HFJ_RESOURCE.RES_ID** column is used as the resource ID for all server-assigned IDs. For example, if a Patient resource is created in a completely empty database, it will be assigned the ID `Patient/1` by the server and RES_ID will have a value of 1.
However, when client-assigned IDs are used, these may contain text values to allow a client to create an ID such as `Patient/ABC`. When a client-assigned ID is given to a resource, a row is created in the **HFJ_RESOURCE** table. When an **HFJ_FORCED_ID** row exists corresponding to the equivalent **HFJ_RESOURCE** row, the RES_ID value is no longer visible or usable by FHIR clients and it becomes purely an internal ID to the JPA server.
If the server has been configured with a [Resource Server ID Strategy](/apidocs/hapi-fhir-jpaserver-api/undefined/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setResourceServerIdStrategy(ca.uhn.fhir.jpa.api.config.DaoConfig.IdStrategyEnum)) of [UUID](/apidocs/hapi-fhir-jpaserver-api/undefined/ca/uhn/fhir/jpa/api/config/DaoConfig.IdStrategyEnum.html#UUID), or the server has been configured with a [Resource Client ID Strategy](/apidocs/hapi-fhir-jpaserver-api/undefined/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setResourceClientIdStrategy(ca.uhn.fhir.jpa.api.config.DaoConfig.ClientIdStrategyEnum)) of [ANY](/apidocs/hapi-fhir-jpaserver-api/undefined/ca/uhn/fhir/jpa/api/config/DaoConfig.ClientIdStrategyEnum.html#ANY) the server will create a Forced ID for all resources (not only resources having textual IDs).
## Columns
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Relationships</th>
<th>Datatype</th>
<th>Nullable</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>PARTITION_ID</td>
<td></td>
<td>Integer</td>
<td>Nullable</td>
<td>
This is the optional partition ID, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
</td>
</tr>
<tr>
<td>PARTITION_DATE</td>
<td></td>
<td>Timestamp</td>
<td>Nullable</td>
<td>
This is the optional partition date, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
</td>
</tr>
<tr>
<td>PID</td>
<td>PK</td>
<td>Long</td>
<td></td>
<td>
This is the row persistent ID.
</td>
</tr>
<tr>
<td>RESOURCE_PID</td>
<td>FK to <a href="#HFJ_RESOURCE">HFJ_RESOURCE</a></td>
<td>Long</td>
<td></td>
<td>
This is the persistent ID of the resource being versioned.
</td>
</tr>
<tr>
<td>FORCED_ID</td>
<td></td>
<td>String</td>
<td></td>
<td>
Contains the specific version (starting with 1) of the resource that this row corresponds to.
</td>
</tr>
</tbody>
</table>
<a name="HFJ_RES_LINK"/>
# HFJ_RES_LINK: Search Links
<img src="/hapi-fhir/docs/images/jpa_erd_resource_links.svg" alt="Resources" style="width: 100%; max-width: 600px;"/>
When a resource is created or updated, it is indexed for searching. Any search parameters of type [Reference](http://hl7.org/fhir/search.html#reference) are resolved, and one or more rows may be created in the **HFJ_RES_LINK** table.
## Columns
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Relationships</th>
<th>Datatype</th>
<th>Nullable</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>PARTITION_ID</td>
<td></td>
<td>Integer</td>
<td>Nullable</td>
<td>
This is the optional partition ID, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
Note that the partition indicated by the <b>PARTITION_ID</b> and <b>PARTITION_DATE</b> columns refers to the partition
of the <i>SOURCE</i> resource, and not necessarily the <i>TARGET</i>.
</td>
</tr>
<tr>
<td>PARTITION_DATE</td>
<td></td>
<td>Timestamp</td>
<td>Nullable</td>
<td>
This is the optional partition date, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
Note that the partition indicated by the <b>PARTITION_ID</b> and <b>PARTITION_DATE</b> columns refers to the partition
of the <i>SOURCE</i> resource, and not necessarily the <i>TARGET</i>.
</td>
</tr>
<tr>
<td>PID</td>
<td></td>
<td>Long</td>
<td></td>
<td>
Holds the persistent ID
</td>
</tr>
<tr>
<td>SRC_PATH</td>
<td></td>
<td>String</td>
<td></td>
<td>
Contains the FHIRPath expression within the source resource containing the path to the target resource, as supplied by the SearchParameter resource that defined the link.
</td>
</tr>
<tr>
<td>SRC_RESOURCE_ID</td>
<td></td>
<td>Long</td>
<td></td>
<td>
Contains a FK reference to the resource containing the link to the target resource.
</td>
</tr>
<tr>
<td>TARGET_RESOURCE_ID</td>
<td></td>
<td>Long</td>
<td>Nullable</td>
<td>
Contains a FK reference to the resource that is the target resource. Will not be populated if the link contains
a reference to an external resource, or a canonical reference.
</td>
</tr>
<tr>
<td>TARGET_RESOURCE_URL</td>
<td></td>
<td>String</td>
<td>Nullable</td>
<td>
If this row contains a reference to an external resource or a canonical reference, this column will contain
the absolute URL.
</td>
</tr>
<tr>
<td>SP_UPDATED</td>
<td></td>
<td>Timestamp</td>
<td></td>
<td>
Contains the last updated timestamp for this row.
</td>
</tr>
</tbody>
</table>
<a name="search-indexes"/>
# 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. 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.
<a name="search-hashes"/>
## 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
<img src="/hapi-fhir/docs/images/jpa_erd_search_indexes.svg" alt="Search Indexes" style="width: 100%; max-width: 900px;"/>
## Columns
The following columns are common to **all HFJ_SPIDX_xxx tables**.
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Relationships</th>
<th>Datatype</th>
<th>Nullable</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>PARTITION_ID</td>
<td></td>
<td>Integer</td>
<td>Nullable</td>
<td>
This is the optional partition ID, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
Note that the partition indicated by the <b>PARTITION_ID</b> and <b>PARTITION_DATE</b> columns refers to the partition
of the <i>SOURCE</i> resource, and not necessarily the <i>TARGET</i>.
</td>
</tr>
<tr>
<td>PARTITION_DATE</td>
<td></td>
<td>Timestamp</td>
<td>Nullable</td>
<td>
This is the optional partition date, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
Note that the partition indicated by the <b>PARTITION_ID</b> and <b>PARTITION_DATE</b> columns refers to the partition
of the <i>SOURCE</i> resource, and not necessarily the <i>TARGET</i>.
</td>
</tr>
<tr>
<td>SP_ID</td>
<td></td>
<td>Long</td>
<td></td>
<td>
Holds the persistent ID
</td>
</tr>
<tr>
<td>RES_ID</td>
<td>FK to <a href="#HFJ_RESOURCE">HFJ_RESOURCE</a></td>
<td>String</td>
<td></td>
<td>
Contains the PID of the resource being indexed.
</td>
</tr>
<tr>
<td>SP_NAME</td>
<td></td>
<td>String</td>
<td></td>
<td>
This is the name of the search parameter being indexed.
</td>
</tr>
<tr>
<td>RES_TYPE</td>
<td></td>
<td>String</td>
<td></td>
<td>
This is the name of the resource being indexed.
</td>
</tr>
<tr>
<td>SP_UPDATED</td>
<td></td>
<td>Timestamp</td>
<td></td>
<td>
This is the time that this row was last updated.
</td>
</tr>
<tr>
<td>SP_MISSING</td>
<td></td>
<td>boolean</td>
<td></td>
<td>
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`.
</td>
</tr>
</tbody>
</table>

View File

@ -4,7 +4,7 @@ If you wish to allow a single endpoint to support multiple tenants, you may supp
This means that additional logic will be performed during request parsing to determine a tenant ID, which will be supplied to resource providers. This can be useful in servers that have multiple distinct logical pools of resources hosted on the same infrastructure.
## URL Base Multitenancy
# URL Base Multitenancy
Using URL Base Multitenancy means that an additional element is added to the path of each resource between the server base URL and the resource name. For example, if your restful server is deployed to `http://acme.org:8080/baseDstu3` and a client wishes to access Patient 123 for Tenant "FOO", the resource ID (and URL to fetch that resource) would be `http://acme.org:8080/FOO/Patient/123`.

View File

@ -119,4 +119,9 @@ public class JaxRsHttpRequest implements IHttpRequest {
return ""; // TODO: can we get this from somewhere?
}
@Override
public void setUri(String theUrl) {
throw new UnsupportedOperationException();
}
}

View File

@ -168,7 +168,7 @@ public class AbstractJaxRsResourceProviderDstu3Test {
@Test
public void testDeletePatient() {
when(mock.delete(idCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome());
final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute();
final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute().getOperationOutcome();
assertEquals("1", idCaptor.getValue().getIdPart());
}

View File

@ -164,7 +164,7 @@ public class AbstractJaxRsResourceProviderTest {
@Test
public void testDeletePatient() {
when(mock.delete(idCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome());
final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute();
final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute().getOperationOutcome();
assertEquals("1", idCaptor.getValue().getIdPart());
}

View File

@ -187,6 +187,7 @@ public class DaoConfig {
*/
private boolean myDeleteEnabled = true;
/**
* Constructor
*/

View File

@ -37,7 +37,7 @@ public interface ISearchCoordinatorSvc {
List<ResourcePersistentId> getResources(String theUuid, int theFrom, int theTo, @Nullable RequestDetails theRequestDetails);
IBundleProvider registerSearch(IFhirResourceDao theCallingDao, SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, @Nullable RequestDetails theRequestDetails);
IBundleProvider registerSearch(IFhirResourceDao<?> theCallingDao, SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, @Nullable RequestDetails theRequestDetails);
/**
* Fetch the total number of search results for the given currently executing search, if one is currently executing and

View File

@ -241,18 +241,6 @@
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>javax.xml.bind</groupId>-->
<!-- <artifactId>jaxb-api</artifactId>-->
<!-- </dependency>-->
<!--<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
</dependency>-->
<!-- Test Database -->
<dependency>
@ -260,11 +248,6 @@
<artifactId>derby</artifactId>
<scope>test</scope>
</dependency>
<!-- <dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbyshared</artifactId>
<scope>test</scope>
</dependency>-->
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbytools</artifactId>
@ -324,15 +307,7 @@
</dependency>
<!-- <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.3.2</version> </dependency> -->
<!-- Spring -->
<!--
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
</dependency>
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
@ -375,12 +350,6 @@
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<!--
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-core</artifactId>
</dependency>
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
@ -427,6 +396,16 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-java8</artifactId>
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
@ -438,7 +417,7 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<exclusions>
<exclusion>
@ -456,10 +435,6 @@
<artifactId>javax.activation</artifactId>
<version>1.2.0</version>
</dependency>
<!--<dependency>-->
<!--<groupId>javax.transaction</groupId>-->
<!--<artifactId>javax.transaction-api</artifactId>-->
<!--</dependency>-->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
@ -476,14 +451,6 @@
</exclusion>
</exclusions>
</dependency>
<!--<dependency>-->
<!--<groupId>com.sun.activation</groupId>-->
<!--<artifactId>javax.activation</artifactId>-->
<!--</dependency>-->
<!--<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>-->
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>

View File

@ -231,7 +231,7 @@ public class BulkDataExportSvcImpl implements IBulkDataExportSvc {
map.setLastUpdated(new DateRangeParam(job.getSince(), null));
}
IResultIterator resultIterator = sb.createQuery(map, new SearchRuntimeDetails(null, theJobUuid), null);
IResultIterator resultIterator = sb.createQuery(map, new SearchRuntimeDetails(null, theJobUuid), null, null);
storeResultsToFiles(nextCollection, sb, resultIterator, jobResourceCounter, jobStopwatch);
}

View File

@ -16,6 +16,11 @@ 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.IPartitionLookupSvc;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperService;
import ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl;
import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
import ca.uhn.fhir.jpa.partition.RequestPartitionHelperService;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.sched.AutowiringSpringBeanJobFactory;
@ -35,10 +40,9 @@ 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 ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.springframework.beans.factory.annotation.Autowired;
@ -208,6 +212,11 @@ public abstract class BaseConfig {
return new HapiFhirHibernateJpaDialect(fhirContext().getLocalizer());
}
@Bean
public IRequestPartitionHelperService requestPartitionHelperService() {
return new RequestPartitionHelperService();
}
@Bean
public PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() {
return new PersistenceExceptionTranslationPostProcessor();
@ -230,11 +239,28 @@ public abstract class BaseConfig {
return new JpaConsentContextServices();
}
@Bean
@Lazy
public IPartitionLookupSvc partitionConfigSvc() {
return new PartitionLookupSvcImpl();
}
@Bean
@Lazy
public PartitionManagementProvider partitionManagementProvider() {
return new PartitionManagementProvider();
}
@Bean
@Lazy
public RequestTenantPartitionInterceptor requestTenantPartitionInterceptor() {
return new RequestTenantPartitionInterceptor();
}
@Bean
@Lazy
public TerminologyUploaderProvider terminologyUploaderProvider() {
TerminologyUploaderProvider retVal = new TerminologyUploaderProvider();
return retVal;
return new TerminologyUploaderProvider();
}
@Bean

View File

@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.entity.ResourceSearchView;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
@ -188,6 +189,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
private SearchBuilderFactory mySearchBuilderFactory;
private FhirContext myContext;
private ApplicationContext myApplicationContext;
@Autowired
private PartitionSettings myPartitionSettings;
@Override
protected IInterceptorBroadcaster getInterceptorBroadcaster() {
@ -222,6 +225,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
retVal.setResourceType(theEntity.getResourceType());
retVal.setForcedId(theId.getIdPart());
retVal.setResource(theEntity);
retVal.setPartitionId(theEntity.getPartitionId());
theEntity.setForcedId(retVal);
}
}
@ -634,6 +638,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
}
}
return retVal;
}
@ -695,6 +700,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
}
}
}
return retVal;
}
@ -1090,6 +1096,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
ResourceHistoryProvenanceEntity provenance = new ResourceHistoryProvenanceEntity();
provenance.setResourceHistoryTable(historyEntry);
provenance.setResourceTable(entity);
provenance.setPartitionId(entity.getPartitionId());
if (haveRequestId) {
provenance.setRequestId(left(requestId, Constants.REQUEST_ID_LENGTH));
}

View File

@ -24,40 +24,77 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
import ca.uhn.fhir.jpa.model.entity.BaseTag;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.TagDefinition;
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperService;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils;
import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.*;
import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.ValidationModeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ObjectUtil;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.ReflectionUtil;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.validation.*;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.IInstanceValidatorModule;
import ca.uhn.fhir.validation.IValidationContext;
import ca.uhn.fhir.validation.IValidatorModule;
import ca.uhn.fhir.validation.ValidationOptions;
import ca.uhn.fhir.validation.ValidationResult;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
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.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.PlatformTransactionManager;
@ -73,7 +110,14 @@ import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -100,6 +144,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
private IInstanceValidatorModule myInstanceValidator;
private String myResourceName;
private Class<T> myResourceType;
@Autowired
private IRequestPartitionHelperService myRequestPartitionHelperService;
@Autowired
private PartitionSettings myPartitionSettings;
@Override
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel, RequestDetails theRequest) {
@ -168,7 +216,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE);
}
return doCreate(theResource, theIfNoneExist, thePerformIndexing, theUpdateTimestamp, theRequestDetails);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequestDetails, theResource);
return doCreate(theResource, theIfNoneExist, thePerformIndexing, theUpdateTimestamp, theRequestDetails, requestPartitionId);
}
@Override
@ -190,7 +239,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
validateIdPresentForDelete(theId);
validateDeleteEnabled();
final ResourceTable entity = readEntityLatestVersion(theId);
final ResourceTable entity = readEntityLatestVersion(theId, theRequest);
if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) {
throw new ResourceVersionConflictException("Trying to delete " + theId + " but this is not the current version");
}
@ -397,13 +446,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
}
private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing, Date theUpdateTime, RequestDetails theRequest) {
private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing, Date theUpdateTime, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
StopWatch w = new StopWatch();
preProcessResourceForStorage(theResource);
ResourceTable entity = new ResourceTable();
entity.setResourceType(toResourceName(theResource));
entity.setPartitionId(theRequestPartitionId);
if (isNotBlank(theIfNoneExist)) {
Set<ResourcePersistentId> match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType, theRequest);
@ -452,7 +502,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails);
}
// Notify JPA interceptors
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequest)
@ -635,6 +685,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public IBundleProvider history(Date theSince, Date theUntil, RequestDetails theRequestDetails) {
if (myPartitionSettings.isPartitioningEnabled()) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "noSystemOrTypeHistoryForPartitionAwareServer");
throw new MethodNotAllowedException(msg);
}
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
notifyInterceptors(RestOperationTypeEnum.HISTORY_TYPE, requestDetails);
@ -708,7 +763,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
throw new ResourceNotFoundException(theResourceId);
}
ResourceTable latestVersion = readEntityLatestVersion(theResourceId);
ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest);
if (latestVersion.getVersion() != entity.getVersion()) {
doMetaAdd(theMetaAdd, entity);
} else {
@ -740,7 +795,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
throw new ResourceNotFoundException(theResourceId);
}
ResourceTable latestVersion = readEntityLatestVersion(theResourceId);
ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest);
if (latestVersion.getVersion() != entity.getVersion()) {
doMetaDelete(theMetaDel, entity);
} else {
@ -816,7 +871,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
} else {
entityToUpdate = readEntityLatestVersion(theId);
entityToUpdate = readEntityLatestVersion(theId, theRequest);
if (theId.hasVersionIdPart()) {
if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
throw new ResourceVersionConflictException("Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch");
@ -943,7 +998,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public BaseHasResource readEntity(IIdType theId, RequestDetails theRequest) {
return readEntity(theId, true, theRequest);
}
@ -951,9 +1005,28 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest) {
validateResourceTypeAndThrowInvalidRequestException(theId);
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(getResourceName(), theId.getIdPart());
@Nullable RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName());
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(requestPartitionId, getResourceName(), theId.getIdPart());
BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid.getIdAsLong());
// Verify that the resource is for the correct partition
if (requestPartitionId != null) {
if (requestPartitionId.getPartitionId() == null) {
if (entity.getPartitionId() != null) {
ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId());
entity = null;
}
} else if (entity.getPartitionId() != null) {
if (!entity.getPartitionId().getPartitionId().equals(requestPartitionId.getPartitionId())) {
ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId());
entity = null;
}
} else {
ourLog.debug("Performing a read for PartitionId=null but entity has partition: {}", entity.getPartitionId());
entity = null;
}
}
if (entity == null) {
throw new ResourceNotFoundException(theId);
}
@ -989,8 +1062,17 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return entity;
}
protected ResourceTable readEntityLatestVersion(IIdType theId) {
ResourcePersistentId persistentId = myIdHelperService.resolveResourcePersistentIds(getResourceName(), theId.getIdPart());
@NotNull
protected ResourceTable readEntityLatestVersion(IIdType theId, RequestDetails theRequestDetails) {
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, getResourceName());
return readEntityLatestVersion(theId, requestPartitionId);
}
@NotNull
private ResourceTable readEntityLatestVersion(IIdType theId, @Nullable RequestPartitionId theRequestPartitionId) {
validateResourceTypeAndThrowInvalidRequestException(theId);
ResourcePersistentId persistentId = myIdHelperService.resolveResourcePersistentIds(theRequestPartitionId, getResourceName(), theId.getIdPart());
ResourceTable entity = myEntityManager.find(ResourceTable.class, persistentId.getId());
if (entity == null) {
throw new ResourceNotFoundException(theId);
@ -1126,7 +1208,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
String uuid = UUID.randomUUID().toString();
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest)) {
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName());
try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) {
while (iter.hasNext()) {
retVal.add(iter.next());
}
@ -1230,10 +1314,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
*/
resourceId = theResource.getIdElement();
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource);
try {
entity = readEntityLatestVersion(resourceId);
entity = readEntityLatestVersion(resourceId, requestPartitionId);
} catch (ResourceNotFoundException e) {
return doCreate(theResource, null, thePerformIndexing, new Date(), theRequest);
return doCreate(theResource, null, thePerformIndexing, new Date(), theRequest, requestPartitionId);
}
}
@ -1303,7 +1388,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
if (theId == null || theId.hasIdPart() == false) {
throw new InvalidRequestException("No ID supplied. ID is required when validating with mode=DELETE");
}
final ResourceTable entity = readEntityLatestVersion(theId);
final ResourceTable entity = readEntityLatestVersion(theId, theRequest);
// Validate that there are no resources pointing to the candidate that
// would prevent deletion

View File

@ -3,10 +3,12 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.util.ResourceCountCache;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.StopWatch;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -47,6 +49,8 @@ public abstract class BaseHapiFhirSystemDao<T, MT> extends BaseHapiFhirDao<IBase
@Autowired
@Qualifier("myResourceCountsCache")
public ResourceCountCache myResourceCountsCache;
@Autowired
private PartitionSettings myPartitionSettings;
@Override
@Transactional(propagation = Propagation.NEVER)
@ -76,6 +80,11 @@ public abstract class BaseHapiFhirSystemDao<T, MT> extends BaseHapiFhirDao<IBase
@Override
public IBundleProvider history(Date theSince, Date theUntil, RequestDetails theRequestDetails) {
if (myPartitionSettings.isPartitioningEnabled()) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "noSystemOrTypeHistoryForPartitionAwareServer");
throw new MethodNotAllowedException(msg);
}
if (theRequestDetails != null) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);

View File

@ -171,6 +171,10 @@ public abstract class BaseStorageDao {
JpaInterceptorBroadcaster.doCallHooks(getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams);
}
protected Object doCallHooksAndReturnObject(RequestDetails theRequestDetails, Pointcut thePointcut, HookParams theParams) {
return JpaInterceptorBroadcaster.doCallHooksAndReturnObject(getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams);
}
protected abstract IInterceptorBroadcaster getInterceptorBroadcaster();
public IBaseOperationOutcome createErrorOperationOutcome(String theMessage, String theCode) {

View File

@ -26,8 +26,6 @@ import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoSearchParameterR4;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.SearchParameter;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum;
@ -40,8 +38,6 @@ import java.util.List;
public class FhirResourceDaoSearchParameterDstu2 extends BaseHapiFhirResourceDao<SearchParameter> implements IFhirResourceDaoSearchParameter<SearchParameter> {
@Autowired
private IFhirSystemDao<Bundle, MetaDt> mySystemDao;
@Autowired
private ISearchParamExtractor mySearchParamExtractor;
@ -81,8 +77,9 @@ public class FhirResourceDaoSearchParameterDstu2 extends BaseHapiFhirResourceDao
String expression = theResource.getXpath();
FhirContext context = getContext();
SearchParamTypeEnum type = theResource.getTypeElement().getValueAsEnum();
String code = theResource.getCode();
FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamExtractor, type, status, base, expression, context, getConfig());
FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamRegistry, mySearchParamExtractor, code, type, status, base, expression, context, getConfig());
}

View File

@ -51,7 +51,7 @@ public class FhirResourceDaoSubscriptionDstu2 extends BaseHapiFhirResourceDao<Su
@Override
public Long getSubscriptionTablePidForSubscriptionResource(IIdType theId, RequestDetails theRequest) {
ResourceTable entity = readEntityLatestVersion(theId);
ResourceTable entity = readEntityLatestVersion(theId, theRequest);
SubscriptionTable table = mySubscriptionTableDao.findOneByResourcePid(entity.getId());
if (table == null) {
return null;

View File

@ -20,11 +20,14 @@ package ca.uhn.fhir.jpa.dao;
* #L%
*/
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
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.PartitionSettings;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
@ -40,7 +43,10 @@ import org.apache.commons.lang3.Validate;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.highlight.Formatter;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.Scorer;
import org.apache.lucene.search.highlight.TokenGroup;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.query.dsl.BooleanJunction;
@ -54,7 +60,12 @@ import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -270,6 +281,12 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
return doSearch(theResourceName, theParams, null);
}
@Autowired
private IRequestPartitionHelperService myRequestPartitionHelperService;
@Autowired
private PartitionSettings myPartitionSettings;
@Transactional()
@Override
public List<Suggestion> suggestKeywords(String theContext, String theSearchParam, String theText, RequestDetails theRequest) {
@ -283,7 +300,12 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
if (contextParts.length != 3 || "Patient".equals(contextParts[0]) == false || "$everything".equals(contextParts[2]) == false) {
throw new InvalidRequestException("Invalid context: " + theContext);
}
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(contextParts[0], contextParts[1]);
// Partitioning is not supported for this operation
Validate.isTrue(myPartitionSettings.isPartitioningEnabled() == false, "Suggest keywords not supported for partitioned system");
RequestPartitionId requestPartitionId = null;
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(requestPartitionId, contextParts[0], contextParts[1]);
FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager);
@ -299,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();

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.dao;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
@ -37,16 +38,16 @@ import java.util.Set;
public interface ISearchBuilder {
IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntime, RequestDetails theRequest);
IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntime, RequestDetails theRequest, RequestPartitionId theRequestPartitionId);
Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest, RequestPartitionId theRequestPartitionId);
void setMaxResultsToFetch(Integer theMaxResultsToFetch);
Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest);
void loadResourcesByPid(Collection<ResourcePersistentId> thePids, Collection<ResourcePersistentId> theIncludedPids, List<IBaseResource> theResourceListToPopulate, boolean theForHistoryOperation, RequestDetails theDetails);
Set<ResourcePersistentId> loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection<ResourcePersistentId> theMatches, Set<Include> theRevIncludes, boolean theReverseMode,
DateRangeParam theLastUpdated, String theSearchIdOrDescription, RequestDetails theRequest);
DateRangeParam theLastUpdated, String theSearchIdOrDescription, RequestDetails theRequest);
/**
* How many results may be fetched at once

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
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.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.IDao;
import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao;
@ -39,6 +40,7 @@ import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinEnum;
import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinKey;
import ca.uhn.fhir.jpa.entity.ResourceSearchView;
import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique;
@ -91,7 +93,6 @@ import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
@ -136,10 +137,11 @@ public class SearchBuilder implements ISearchBuilder {
private static final List<ResourcePersistentId> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>());
private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class);
private static ResourcePersistentId NO_MORE = new ResourcePersistentId(-1L);
private static final ResourcePersistentId NO_MORE = new ResourcePersistentId(-1L);
private final QueryRoot myQueryRoot = new QueryRoot();
private final String myResourceName;
private final Class<? extends IBaseResource> myResourceType;
private final IDao myCallingDao;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
@ -164,13 +166,15 @@ public class SearchBuilder implements ISearchBuilder {
private PredicateBuilderFactory myPredicateBuilderFactory;
private List<ResourcePersistentId> myAlsoIncludePids;
private CriteriaBuilder myCriteriaBuilder;
private IDao myCallingDao;
private SearchParameterMap myParams;
private String mySearchUuid;
private int myFetchSize;
private Integer myMaxResultsToFetch;
private Set<ResourcePersistentId> myPidSet;
private PredicateBuilder myPredicateBuilder;
private RequestPartitionId myRequestPartitionId;
@Autowired
private PartitionSettings myPartitionSettings;
/**
* Constructor
@ -187,7 +191,7 @@ public class SearchBuilder implements ISearchBuilder {
}
private void searchForIdsWithAndOr(String theResourceName, String theNextParamName, List<List<IQueryParameterType>> theAndOrParams, RequestDetails theRequest) {
myPredicateBuilder.searchForIdsWithAndOr(theResourceName, theNextParamName, theAndOrParams, theRequest);
myPredicateBuilder.searchForIdsWithAndOr(theResourceName, theNextParamName, theAndOrParams, theRequest, myRequestPartitionId);
}
private void searchForIdsWithAndOr(@Nonnull SearchParameterMap theParams, RequestDetails theRequest) {
@ -225,8 +229,8 @@ public class SearchBuilder implements ISearchBuilder {
}
@Override
public Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest) {
init(theParams, theSearchUuid);
public Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
init(theParams, theSearchUuid, theRequestPartitionId);
TypedQuery<Long> query = createQuery(null, null, true, theRequest);
return new CountQueryIterator(query);
@ -236,13 +240,13 @@ public class SearchBuilder implements ISearchBuilder {
* @param thePidSet May be null
*/
@Override
public void setPreviouslyAddedResourcePids(@Nullable List<ResourcePersistentId> thePidSet) {
public void setPreviouslyAddedResourcePids(@Nonnull List<ResourcePersistentId> thePidSet) {
myPidSet = new HashSet<>(thePidSet);
}
@Override
public IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) {
init(theParams, theSearchRuntimeDetails.getSearchUuid());
public IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
init(theParams, theSearchRuntimeDetails.getSearchUuid(), theRequestPartitionId);
if (myPidSet == null) {
myPidSet = new HashSet<>();
@ -251,13 +255,15 @@ public class SearchBuilder implements ISearchBuilder {
return new QueryIterator(theSearchRuntimeDetails, theRequest);
}
private void init(SearchParameterMap theParams, String theSearchUuid) {
private void init(SearchParameterMap theParams, String theSearchUuid, RequestPartitionId theRequestPartitionId) {
myParams = theParams;
myCriteriaBuilder = myEntityManager.getCriteriaBuilder();
mySearchUuid = theSearchUuid;
myPredicateBuilder = new PredicateBuilder(this, myPredicateBuilderFactory);
myRequestPartitionId = theRequestPartitionId;
}
private TypedQuery<Long> createQuery(SortSpec sort, Integer theMaximumResults, boolean theCount, RequestDetails theRequest) {
CriteriaQuery<Long> outerQuery;
/*
@ -302,7 +308,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myParams.get(IAnyResource.SP_RES_ID) != null) {
StringParam idParam = (StringParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0);
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(myResourceName, idParam.getValue());
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, idParam.getValue());
if (myAlsoIncludePids == null) {
myAlsoIncludePids = new ArrayList<>(1);
}
@ -372,12 +378,18 @@ public class SearchBuilder implements ISearchBuilder {
* If we have any joins to index tables, we get this behaviour already guaranteed so we don't
* need an explicit predicate for it.
*/
boolean haveNoIndexSearchParams = myParams.size() == 0 || myParams.keySet().stream().allMatch(t -> t.startsWith("_"));
if (haveNoIndexSearchParams) {
if (!myQueryRoot.hasIndexJoins()) {
if (myParams.getEverythingMode() == null) {
myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myResourceType"), myResourceName));
}
myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myDeleted")));
if (myRequestPartitionId != null) {
if (myRequestPartitionId.getPartitionId() != null) {
myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myPartitionIdValue").as(Integer.class), myRequestPartitionId.getPartitionId()));
} else {
myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myPartitionIdValue").as(Integer.class)));
}
}
}
// Last updated
@ -500,7 +512,7 @@ public class SearchBuilder implements ISearchBuilder {
Predicate joinParam1 = theBuilder.equal(join.get("myParamName"), theSort.getParamName());
theQueryRoot.addPredicate(joinParam1);
} else {
Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myResourceName, theSort.getParamName());
Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myPartitionSettings, myRequestPartitionId, myResourceName, theSort.getParamName());
Predicate joinParam1 = theBuilder.equal(join.get("myHashIdentity"), hashIdentity);
theQueryRoot.addPredicate(joinParam1);
}
@ -889,7 +901,7 @@ public class SearchBuilder implements ISearchBuilder {
.add(StorageProcessingMessage.class, msg);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
addPredicateCompositeStringUnique(theParams, indexString);
addPredicateCompositeStringUnique(theParams, indexString, myRequestPartitionId);
}
}
}
@ -904,9 +916,16 @@ public class SearchBuilder implements ISearchBuilder {
}
}
private void addPredicateCompositeStringUnique(@Nonnull SearchParameterMap theParams, String theIndexedString) {
myQueryRoot.setHasIndexJoins(true);
private void addPredicateCompositeStringUnique(@Nonnull SearchParameterMap theParams, String theIndexedString, RequestPartitionId theRequestPartitionId) {
Join<ResourceTable, ResourceIndexedCompositeStringUnique> join = myQueryRoot.join("myParamsCompositeStringUnique", JoinType.LEFT);
if (theRequestPartitionId != null) {
Integer partitionId = theRequestPartitionId.getPartitionId();
Predicate predicate = myCriteriaBuilder.equal(join.get("myPartitionIdValue").as(Integer.class), partitionId);
myQueryRoot.addPredicate(predicate);
}
myQueryRoot.setHasIndexJoins();
Predicate predicate = myCriteriaBuilder.equal(join.get("myIndexString"), theIndexedString);
myQueryRoot.addPredicate(predicate);
@ -949,10 +968,6 @@ public class SearchBuilder implements ISearchBuilder {
return myResourceName;
}
public IDao getCallingDao() {
return myCallingDao;
}
@VisibleForTesting
public void setDaoConfigForUnitTest(DaoConfig theDaoConfig) {
myDaoConfig = theDaoConfig;

View File

@ -25,17 +25,19 @@ import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParserErrorHandler;
import ca.uhn.fhir.parser.JsonParser;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Iterator;
import java.util.Map;
import static org.apache.commons.lang3.StringUtils.defaultString;
class TolerantJsonParser extends JsonParser {
private static final Logger ourLog = LoggerFactory.getLogger(TolerantJsonParser.class);
TolerantJsonParser(FhirContext theContext, IParserErrorHandler theParserErrorHandler) {
super(theContext, theParserErrorHandler);
}
@ -45,31 +47,33 @@ class TolerantJsonParser extends JsonParser {
try {
return super.parseResource(theResourceType, theMessageString);
} catch (DataFormatException e) {
if (defaultString(e.getMessage()).contains("Unexpected character ('.' (code 46))")) {
/*
* The following is a hacky and gross workaround until the following PR is hopefully merged:
* https://github.com/FasterXML/jackson-core/pull/611
*
* The issue this solves is that under Gson it was possible to store JSON containing
* decimal numbers with no leading integer, e.g. .123
*
* These don't parse in Jackson, meaning we can be stuck with data in the database
* that can't be loaded back out.
*
* Note that if we fix this in the future to rely on Jackson natively handing this
* nicely we may or may not be able to remove some code from
* ParserState.Primitive state too.
*/
/*
* The following is a hacky and gross workaround until the following PR is hopefully merged:
* https://github.com/FasterXML/jackson-core/pull/611
*
* The issue this solves is that under Gson it was possible to store JSON containing
* decimal numbers with no leading integer (e.g. .123) and numbers with double leading
* zeros (e.g. 000.123).
*
* These don't parse in Jackson (which is valid behaviour, these aren't ok according to the
* JSON spec), meaning we can be stuck with data in the database that can't be loaded back out.
*
* Note that if we fix this in the future to rely on Jackson natively handing this
* nicely we may or may not be able to remove some code from
* ParserState.Primitive state too.
*/
String msg = defaultString(e.getMessage());
if (msg.contains("Unexpected character ('.' (code 46))") || msg.contains("Invalid numeric value: Leading zeroes not allowed")) {
Gson gson = new Gson();
JsonObject object = gson.fromJson(theMessageString, JsonObject.class);
String corrected = gson.toJson(object);
return super.parseResource(theResourceType, corrected);
}
throw e;
}
}

View File

@ -39,6 +39,12 @@ public interface IForcedIdDao extends JpaRepository<ForcedId, Long> {
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId = :forced_id")
Optional<Long> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId);
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myPartitionId.myPartitionId IS NULL AND myResourceType = :resource_type AND myForcedId = :forced_id")
Optional<Long> findByPartitionIdNullAndTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId);
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myPartitionId.myPartitionId = :partition_id AND myResourceType = :resource_type AND myForcedId = :forced_id")
Optional<Long> findByPartitionIdAndTypeAndForcedId(@Param("partition_id") Integer thePartitionId, @Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId);
@Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid")
ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid);
@ -53,6 +59,20 @@ public interface IForcedIdDao extends JpaRepository<ForcedId, Long> {
@Query("SELECT f.myForcedId, f.myResourcePid FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId IN ( :forced_id )")
Collection<Object[]> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedId);
/**
* This method returns a Collection where each row is an element in the collection. Each element in the collection
* is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way.
*/
@Query("SELECT f.myForcedId, f.myResourcePid FROM ForcedId f WHERE myPartitionIdValue = :partition_id AND myResourceType = :resource_type AND myForcedId IN ( :forced_id )")
Collection<Object[]> findByTypeAndForcedIdInPartition(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedId, @Param("partition_id") Integer thePartitionId);
/**
* This method returns a Collection where each row is an element in the collection. Each element in the collection
* is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way.
*/
@Query("SELECT f.myForcedId, f.myResourcePid FROM ForcedId f WHERE myPartitionIdValue IS NULL AND myResourceType = :resource_type AND myForcedId IN ( :forced_id )")
Collection<Object[]> findByTypeAndForcedIdInPartitionNull(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedId);
/**
* Warning: No DB index exists for this particular query, so it may not perform well
*
@ -78,4 +98,31 @@ public interface IForcedIdDao extends JpaRepository<ForcedId, Long> {
"JOIN ResourceTable t ON t.myId = f.myResourcePid " +
"WHERE f.myResourceType = :resource_type AND f.myForcedId IN ( :forced_id )")
Collection<Object[]> findAndResolveByForcedIdWithNoType(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedIds);
/**
* This method returns a Collection where each row is an element in the collection. Each element in the collection
* is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way.
*/
@Query("" +
"SELECT " +
" f.myResourceType, f.myResourcePid, f.myForcedId, t.myDeleted " +
"FROM ForcedId f " +
"JOIN ResourceTable t ON t.myId = f.myResourcePid " +
"WHERE f.myResourceType = :resource_type AND f.myForcedId IN ( :forced_id ) AND f.myPartitionIdValue = :partition_id")
Collection<Object[]> findAndResolveByForcedIdWithNoTypeInPartition(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedIds, @Param("partition_id") Integer thePartitionId);
/**
* This method returns a Collection where each row is an element in the collection. Each element in the collection
* is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way.
*/
@Query("" +
"SELECT " +
" f.myResourceType, f.myResourcePid, f.myForcedId, t.myDeleted " +
"FROM ForcedId f " +
"JOIN ResourceTable t ON t.myId = f.myResourcePid " +
"WHERE f.myResourceType = :resource_type AND f.myForcedId IN ( :forced_id ) AND f.myPartitionIdValue IS NULL")
Collection<Object[]> findAndResolveByForcedIdWithNoTypeInPartitionNull(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedIds);
}

View File

@ -0,0 +1,36 @@
package ca.uhn.fhir.jpa.dao.data;
import ca.uhn.fhir.jpa.entity.PartitionEntity;
import org.checkerframework.checker.nullness.Opt;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* 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%
*/
public interface IPartitionDao extends JpaRepository<PartitionEntity, Integer> {
@Query("SELECT p FROM PartitionEntity p WHERE p.myName = :name")
Optional<PartitionEntity> findForName(@Param("name") String theName);
}

View File

@ -25,15 +25,13 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Collection;
import java.util.Optional;
import java.util.List;
public interface IResourceIndexedCompositeStringUniqueDao extends JpaRepository<ResourceIndexedCompositeStringUnique, Long> {
@Query("SELECT r FROM ResourceIndexedCompositeStringUnique r WHERE r.myIndexString = :str")
ResourceIndexedCompositeStringUnique findByQueryString(@Param("str") String theQueryString);
@Query("SELECT r.myResourceId FROM ResourceIndexedCompositeStringUnique r WHERE r.myIndexString IN :str")
Collection<Long> findResourcePidsByQueryStrings(@Param("str") Collection<String> theQueryString);
@Query("SELECT r FROM ResourceIndexedCompositeStringUnique r WHERE r.myResourceId = :resId")
List<ResourceIndexedCompositeStringUnique> findAllForResourceIdForUnitTest(@Param("resId") Long theResourceId);
}

View File

@ -20,15 +20,19 @@ package ca.uhn.fhir.jpa.dao.data;
* #L%
*/
import org.springframework.data.jpa.repository.JpaRepository;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface IResourceIndexedSearchParamDateDao extends JpaRepository<ResourceIndexedSearchParamDate, Long> {
@Modifying
@Query("delete from ResourceIndexedSearchParamDate t WHERE t.myResourcePid = :resid")
void deleteByResourceId(@Param("resid") Long theResourcePid);
@Query("SELECT t FROM ResourceIndexedSearchParamDate t WHERE t.myResourcePid = :resId")
List<ResourceIndexedSearchParamDate> findAllForResourceId(@Param("resId") Long thePatientId);
}

View File

@ -20,9 +20,8 @@ package ca.uhn.fhir.jpa.dao.data;
* #L%
*/
import org.springframework.data.jpa.repository.JpaRepository;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@ -32,6 +31,9 @@ import java.util.List;
public interface IResourceIndexedSearchParamStringDao extends JpaRepository<ResourceIndexedSearchParamString, Long> {
@Modifying
@Query("delete from ResourceIndexedSearchParamString t WHERE t.myResourcePid = :resid")
void deleteByResourceId(@Param("resid") Long theResourcePid);
@Query("DELETE FROM ResourceIndexedSearchParamString t WHERE t.myResourcePid = :resId")
void deleteByResourceId(@Param("resId") Long theResourcePid);
@Query("SELECT t FROM ResourceIndexedSearchParamString t WHERE t.myResourcePid = :resId")
List<ResourceIndexedSearchParamString> findAllForResourceId(@Param("resId") Long thePatientId);
}

View File

@ -20,16 +20,20 @@ package ca.uhn.fhir.jpa.dao.data;
* #L%
*/
import org.springframework.data.jpa.repository.JpaRepository;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface IResourceLinkDao extends JpaRepository<ResourceLink, Long> {
import java.util.List;
public interface IResourceLinkDao extends JpaRepository<ResourceLink, Long> {
@Modifying
@Query("delete from ResourceLink t WHERE t.mySourceResourcePid = :resid")
void deleteByResourceId(@Param("resid") Long theResourcePid);
@Query("DELETE FROM ResourceLink t WHERE t.mySourceResourcePid = :resId")
void deleteByResourceId(@Param("resId") Long theResourcePid);
@Query("SELECT t FROM ResourceLink t WHERE t.mySourceResourcePid = :resId")
List<ResourceLink> findAllForResourceId(@Param("resId") Long thePatientId);
}

View File

@ -67,4 +67,10 @@ public interface IResourceTableDao extends JpaRepository<ResourceTable, Long> {
@Query("SELECT t.myResourceType, t.myId, t.myDeleted FROM ResourceTable t WHERE t.myId IN (:pid)")
Collection<Object[]> findLookupFieldsByResourcePid(@Param("pid") List<Long> thePids);
@Query("SELECT t.myResourceType, t.myId, t.myDeleted FROM ResourceTable t WHERE t.myId IN (:pid) AND t.myPartitionIdValue = :partition_id")
Collection<Object[]> findLookupFieldsByResourcePidInPartition(@Param("pid") List<Long> thePids, @Param("partition_id") Integer thePartitionId);
@Query("SELECT t.myResourceType, t.myId, t.myDeleted FROM ResourceTable t WHERE t.myId IN (:pid) AND t.myPartitionIdValue IS NULL")
Collection<Object[]> findLookupFieldsByResourcePidInPartitionNull(@Param("pid") List<Long> thePids);
}

View File

@ -1,7 +1,13 @@
package ca.uhn.fhir.jpa.dao.data;
import java.util.Collection;
import java.util.Date;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.SearchParamPresent;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
/*
* #%L
@ -23,18 +29,10 @@ import java.util.Date;
* #L%
*/
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.SearchParamPresent;
public interface ISearchParamPresentDao extends JpaRepository<SearchParamPresent, Long> {
@Query("SELECT s FROM SearchParamPresent s WHERE s.myResource = :res")
Collection<SearchParamPresent> findAllForResource(@Param("res") ResourceTable theResource);
List<SearchParamPresent> findAllForResource(@Param("res") ResourceTable theResource);
@Modifying
@Query("delete from SearchParamPresent t WHERE t.myResourcePid = :resid")

View File

@ -72,8 +72,9 @@ public class FhirResourceDaoSearchParameterDstu3 extends BaseHapiFhirResourceDao
String expression = theResource.getExpression();
FhirContext context = getContext();
Enumerations.SearchParamType type = theResource.getType();
String code = theResource.getCode();
FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamExtractor, type, status, base, expression, context, getConfig());
FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamRegistry, mySearchParamExtractor, code, type, status, base, expression, context, getConfig());
}
}

View File

@ -48,7 +48,7 @@ public class FhirResourceDaoSubscriptionDstu3 extends BaseHapiFhirResourceDao<Su
@Override
public Long getSubscriptionTablePidForSubscriptionResource(IIdType theId, RequestDetails theRequest) {
ResourceTable entity = readEntityLatestVersion(theId);
ResourceTable entity = readEntityLatestVersion(theId, theRequest);
SubscriptionTable table = mySubscriptionTableDao.findOneByResourcePid(entity.getId());
if (table == null) {
return null;

View File

@ -121,6 +121,7 @@ public class ExpungeEverythingService {
counter.addAndGet(expungeEverythingByType(ResourceHistoryProvenanceEntity.class));
counter.addAndGet(expungeEverythingByType(ResourceHistoryTable.class));
counter.addAndGet(expungeEverythingByType(ResourceTable.class));
counter.addAndGet(expungeEverythingByType(PartitionEntity.class));
myTxTemplate.execute(t -> {
counter.addAndGet(doExpungeEverythingQuery("DELETE from " + org.hibernate.search.jpa.Search.class.getSimpleName() + " d"));
return null;

View File

@ -26,6 +26,7 @@ import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
@ -41,7 +42,6 @@ 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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Nullable;
import javax.persistence.EntityManager;
@ -63,11 +63,11 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
private DaoRegistry myDaoRegistry;
@Override
public IResourceLookup findTargetResource(RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
public IResourceLookup findTargetResource(RequestPartitionId theRequestPartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theResourceType, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
IResourceLookup resolvedResource;
String idPart = theSourceResourceId.getIdPart();
try {
resolvedResource = myIdHelperService.resolveResourceIdentity(theTypeString, idPart, theRequest);
resolvedResource = myIdHelperService.resolveResourceIdentity(theRequestPartitionId, theResourceType, idPart, theRequest);
ourLog.trace("Translated {}/{} to resource PID {}", theType, idPart, resolvedResource);
} catch (ResourceNotFoundException e) {
@ -88,8 +88,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());
}
@ -98,7 +98,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;
}

View File

@ -61,7 +61,6 @@ public class DaoSearchParamSynchronizer {
}
private <T extends BaseResourceIndex> void synchronize(ResourceIndexedSearchParams theParams, ResourceTable theEntity, AddRemoveCount theAddRemoveCount, Collection<T> theNewParms, Collection<T> theExistingParms) {
theParams.calculateHashes(theNewParms);
List<T> quantitiesToRemove = subtract(theExistingParms, theNewParms);
List<T> quantitiesToAdd = subtract(theNewParms, theExistingParms);
tryToReuseIndexEntities(quantitiesToRemove, quantitiesToAdd);
@ -69,6 +68,10 @@ public class DaoSearchParamSynchronizer {
myEntityManager.remove(next);
theEntity.getParamsQuantity().remove(next);
}
for (T next : quantitiesToAdd) {
next.setPartitionId(theEntity.getPartitionId());
}
theParams.calculateHashes(theNewParms);
for (T next : quantitiesToAdd) {
myEntityManager.merge(next);
}
@ -102,7 +105,8 @@ public class DaoSearchParamSynchronizer {
// Take a row we were going to remove, and repurpose its ID
T entityToReuse = theIndexesToRemove.remove(theIndexesToRemove.size() - 1);
targetEntity.setId(entityToReuse.getId());
entityToReuse.copyMutableValuesFrom(targetEntity);
theIndexesToAdd.set(addIndex, entityToReuse);
}
}

View File

@ -21,9 +21,8 @@ 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.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
@ -31,12 +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;
@ -48,9 +45,11 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.stereotype.Service;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collection;
@ -59,10 +58,12 @@ import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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
@ -93,6 +94,8 @@ public class IdHelperService {
private DaoConfig myDaoConfig;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
private FhirContext myFhirCtx;
private Cache<String, Long> myPersistentIdCache;
private Cache<String, IResourceLookup> myResourceLookupCache;
@ -115,10 +118,10 @@ public class IdHelperService {
* @throws ResourceNotFoundException If the ID can not be found
*/
@Nonnull
public IResourceLookup resolveResourceIdentity(String theResourceName, String theResourceId, RequestDetails theRequestDetails) throws ResourceNotFoundException {
public IResourceLookup resolveResourceIdentity(RequestPartitionId theRequestPartitionId, 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);
Collection<IResourceLookup> matches = translateForcedIdToPids(theRequestDetails, Collections.singletonList(id));
IdDt id = new IdDt(theResourceType, theResourceId);
Collection<IResourceLookup> matches = translateForcedIdToPids(theRequestPartitionId, theRequestDetails, Collections.singletonList(id));
assert matches.size() <= 1;
if (matches.isEmpty()) {
throw new ResourceNotFoundException(id);
@ -132,14 +135,14 @@ public class IdHelperService {
* @throws ResourceNotFoundException If the ID can not be found
*/
@Nonnull
public ResourcePersistentId resolveResourcePersistentIds(String theResourceType, String theId) {
public ResourcePersistentId resolveResourcePersistentIds(RequestPartitionId theRequestPartitionId, String theResourceType, String theId) {
Long retVal;
if (myDaoConfig.getResourceClientIdStrategy() == DaoConfig.ClientIdStrategyEnum.ANY || !isValidPid(theId)) {
if (myDaoConfig.isDeleteEnabled()) {
retVal = resolveResourceIdentity(theResourceType, theId);
retVal = resolveResourceIdentity(theRequestPartitionId, theResourceType, theId);
} else {
String key = theResourceType + "/" + theId;
retVal = myPersistentIdCache.get(key, t -> resolveResourceIdentity(theResourceType, theId));
String key = RequestPartitionId.stringifyForKey(theRequestPartitionId) + "/" + theResourceType + "/" + theId;
retVal = myPersistentIdCache.get(key, t -> resolveResourceIdentity(theRequestPartitionId, theResourceType, theId));
}
} else {
@ -150,10 +153,13 @@ public class IdHelperService {
}
/**
* Given a collection of resource IDs (resource type + id), resolves the internal persistent IDs
* Given a collection of resource IDs (resource type + id), resolves the internal persistent IDs.
* <p>
* This implementation will always try to use a cache for performance, meaning that it can resolve resources that
* are deleted (but note that forced IDs can't change, so the cache can't return incorrect results)
*/
@Nonnull
public List<ResourcePersistentId> resolveResourcePersistentIds(List<IIdType> theIds, RequestDetails theRequest) {
public List<ResourcePersistentId> resolveResourcePersistentIdsWithCache(RequestPartitionId theRequestPartitionId, List<IIdType> theIds, RequestDetails theRequest) {
theIds.forEach(id -> Validate.isTrue(id.hasIdPart()));
if (theIds.isEmpty()) {
@ -183,29 +189,35 @@ public class IdHelperService {
} else {
if (!myDaoConfig.isDeleteEnabled()) {
for (Iterator<String> idIterator = nextIds.iterator(); idIterator.hasNext(); ) {
String nextId = idIterator.next();
String key = nextResourceType + "/" + nextId;
Long nextCachedPid = myPersistentIdCache.getIfPresent(key);
if (nextCachedPid != null) {
idIterator.remove();
retVal.add(new ResourcePersistentId(nextCachedPid));
}
for (Iterator<String> idIterator = nextIds.iterator(); idIterator.hasNext(); ) {
String nextId = idIterator.next();
String key = RequestPartitionId.stringifyForKey(theRequestPartitionId) + "/" + nextResourceType + "/" + nextId;
Long nextCachedPid = myPersistentIdCache.getIfPresent(key);
if (nextCachedPid != null) {
idIterator.remove();
retVal.add(new ResourcePersistentId(nextCachedPid));
}
}
if (nextIds.size() > 0) {
Collection<Object[]> views = myForcedIdDao.findByTypeAndForcedId(nextResourceType, nextIds);
Collection<Object[]> views;
if (theRequestPartitionId != null) {
if (theRequestPartitionId.getPartitionId() != null) {
views = myForcedIdDao.findByTypeAndForcedIdInPartition(nextResourceType, nextIds, theRequestPartitionId.getPartitionId());
} else {
views = myForcedIdDao.findByTypeAndForcedIdInPartitionNull(nextResourceType, nextIds);
}
} else {
views = myForcedIdDao.findByTypeAndForcedId(nextResourceType, nextIds);
}
for (Object[] nextView : views) {
String forcedId = (String) nextView[0];
Long pid = (Long) nextView[1];
retVal.add(new ResourcePersistentId(pid));
if (!myDaoConfig.isDeleteEnabled()) {
String key = nextResourceType + "/" + forcedId;
myPersistentIdCache.put(key, pid);
}
String key = RequestPartitionId.stringifyForKey(theRequestPartitionId) + "/" + nextResourceType + "/" + forcedId;
myPersistentIdCache.put(key, pid);
}
}
@ -248,15 +260,35 @@ public class IdHelperService {
return typeToIds;
}
private Long resolveResourceIdentity(String theResourceType, String theId) {
Long retVal;
retVal = myForcedIdDao
.findByTypeAndForcedId(theResourceType, theId)
.orElseThrow(() -> new ResourceNotFoundException(new IdDt(theResourceType, theId)));
return retVal;
private Long resolveResourceIdentity(@Nullable RequestPartitionId theRequestPartitionId, @Nonnull String theResourceType, @Nonnull String theId) {
Optional<Long> pid;
if (theRequestPartitionId != null) {
if (theRequestPartitionId.getPartitionId() == null) {
pid = myForcedIdDao.findByPartitionIdNullAndTypeAndForcedId(theResourceType, theId);
} else {
pid = myForcedIdDao.findByPartitionIdAndTypeAndForcedId(theRequestPartitionId.getPartitionId(), theResourceType, theId);
}
} else {
try {
pid = myForcedIdDao.findByTypeAndForcedId(theResourceType, theId);
} catch (IncorrectResultSizeDataAccessException e) {
/*
* This means that:
* 1. There are two resources with the exact same resource type and forced id
* 2. The unique constraint on this column-pair has been dropped
*/
String msg = myFhirCtx.getLocalizer().getMessage(IdHelperService.class, "nonUniqueForcedId");
throw new PreconditionFailedException(msg);
}
}
if (!pid.isPresent()) {
throw new ResourceNotFoundException(new IdDt(theResourceType, theId));
}
return pid.get();
}
private Collection<IResourceLookup> translateForcedIdToPids(RequestDetails theRequest, Collection<IIdType> theId) {
private Collection<IResourceLookup> translateForcedIdToPids(RequestPartitionId theRequestPartitionId, RequestDetails theRequest, Collection<IIdType> theId) {
theId.forEach(id -> Validate.isTrue(id.hasIdPart()));
if (theId.isEmpty()) {
@ -272,10 +304,7 @@ public class IdHelperService {
.map(t -> t.getIdPartAsLong())
.collect(Collectors.toList());
if (!pids.isEmpty()) {
myResourceTableDao.findLookupFieldsByResourcePid(pids)
.stream()
.map(lookup -> new ResourceLookup((String)lookup[0], (Long)lookup[1], (Date)lookup[2]))
.forEach(retVal::add);
resolvePids(theRequestPartitionId, pids, retVal);
}
}
@ -298,14 +327,16 @@ public class IdHelperService {
if (nextIds.size() > 0) {
Collection<Object[]> views;
if (isBlank(nextResourceType)) {
warnAboutUnqualifiedForcedIdResolution(theRequest);
views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextIds);
assert isNotBlank(nextResourceType);
if (theRequestPartitionId != null) {
if (theRequestPartitionId.getPartitionId() != null) {
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartition(nextResourceType, nextIds, theRequestPartitionId.getPartitionId());
} else {
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(nextResourceType, nextIds);
}
} else {
views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextResourceType, nextIds);
}
for (Object[] next : views) {
@ -328,15 +359,21 @@ 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);
private void resolvePids(RequestPartitionId theRequestPartitionId, List<Long> thePidsToResolve, List<IResourceLookup> theTarget) {
Collection<Object[]> lookup;
if (theRequestPartitionId != null) {
if (theRequestPartitionId.getPartitionId() != null) {
lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartition(thePidsToResolve, theRequestPartitionId.getPartitionId());
} else {
lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionNull(thePidsToResolve);
}
} else {
lookup = myResourceTableDao.findLookupFieldsByResourcePid(thePidsToResolve);
}
lookup
.stream()
.map(t -> new ResourceLookup((String) t[0], (Long) t[1], (Date) t[2]))
.forEach(theTarget::add);
}
public void clearCache() {

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.MatchResourceUrlService;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique;
@ -69,7 +70,8 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Lazy
public class SearchParamWithInlineReferencesExtractor {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamWithInlineReferencesExtractor.class);
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
private MatchResourceUrlService myMatchResourceUrlService;
@Autowired
@ -88,18 +90,17 @@ public class SearchParamWithInlineReferencesExtractor {
private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
private PartitionSettings myPartitionSettings;
public void populateFromResource(ResourceIndexedSearchParams theParams, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams theExistingParams, RequestDetails theRequest) {
extractInlineReferences(theResource, theRequest);
mySearchParamExtractorService.extractFromResource(theRequest, theParams, theEntity, theResource, theUpdateTime, true);
mySearchParamExtractorService.extractFromResource(theEntity.getPartitionId(), theRequest, theParams, theEntity, theResource, theUpdateTime, true);
Set<Map.Entry<String, RuntimeSearchParam>> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet();
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) {
theParams.findMissingSearchParams(myDaoConfig.getModelConfig(), theEntity, activeSearchParams);
theParams.findMissingSearchParams(myPartitionSettings, myDaoConfig.getModelConfig(), theEntity, activeSearchParams);
}
/*
@ -204,7 +205,6 @@ public class SearchParamWithInlineReferencesExtractor {
}
/**
* Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the
* matching resource.

View File

@ -21,8 +21,11 @@ package ca.uhn.fhir.jpa.dao.predicate;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.SearchBuilder;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.BasePartitionable;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
@ -45,21 +48,23 @@ import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.List;
abstract class BasePredicateBuilder {
private static final Logger ourLog = LoggerFactory.getLogger(BasePredicateBuilder.class);
@Autowired
FhirContext myContext;
@Autowired
DaoConfig myDaoConfig;
boolean myDontUseHashesForSearch;
final CriteriaBuilder myCriteriaBuilder;
final QueryRoot myQueryRoot;
final Class<? extends IBaseResource> myResourceType;
final String myResourceName;
final SearchParameterMap myParams;
@Autowired
FhirContext myContext;
@Autowired
DaoConfig myDaoConfig;
boolean myDontUseHashesForSearch;
@Autowired
private PartitionSettings myPartitionSettings;
BasePredicateBuilder(SearchBuilder theSearchBuilder) {
myCriteriaBuilder = theSearchBuilder.getBuilder();
@ -110,41 +115,57 @@ abstract class BasePredicateBuilder {
return (Join<ResourceTable, T>) join;
}
void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing) {
// if (myDontUseHashesForSearch) {
// Join<ResourceTable, SearchParamPresent> paramPresentJoin = myQueryRoot.join("mySearchParamPresents", JoinType.LEFT);
// Join<Object, Object> paramJoin = paramPresentJoin.join("mySearchParam", JoinType.LEFT);
//
// myQueryRoot.addPredicate(myBuilder.equal(paramJoin.get("myResourceName"), theResourceName));
// myQueryRoot.addPredicate(myBuilder.equal(paramJoin.get("myParamName"), theParamName));
// myQueryRoot.addPredicate(myBuilder.equal(paramPresentJoin.get("myPresent"), !theMissing));
// }
void addPredicateParamMissingForReference(String theResourceName, String theParamName, boolean theMissing, RequestPartitionId theRequestPartitionId) {
Join<ResourceTable, SearchParamPresent> paramPresentJoin = myQueryRoot.join("mySearchParamPresents", JoinType.LEFT);
Expression<Long> hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class);
Long hash = SearchParamPresent.calculateHashPresence(theResourceName, theParamName, !theMissing);
myQueryRoot.addPredicate(myCriteriaBuilder.equal(hashPresence, hash));
Long hash = SearchParamPresent.calculateHashPresence(myPartitionSettings, theRequestPartitionId, theResourceName, theParamName, !theMissing);
List<Predicate> predicates = new ArrayList<>();
predicates.add(myCriteriaBuilder.equal(hashPresence, hash));
addPartitionIdPredicate(theRequestPartitionId, paramPresentJoin, predicates);
myQueryRoot.setHasIndexJoins();
myQueryRoot.addPredicates(predicates);
}
void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing, Join<ResourceTable, ? extends BaseResourceIndexedSearchParam> theJoin) {
void addPredicateParamMissingForNonReference(String theResourceName, String theParamName, boolean theMissing, Join<ResourceTable, ? extends BaseResourceIndexedSearchParam> theJoin, RequestPartitionId theRequestPartitionId) {
if (theRequestPartitionId != null) {
if (theRequestPartitionId.getPartitionId() != null) {
myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue"), theRequestPartitionId.getPartitionId()));
} else {
myQueryRoot.addPredicate(myCriteriaBuilder.isNull(theJoin.get("myPartitionIdValue")));
}
}
myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myResourceType"), theResourceName));
myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myParamName"), theParamName));
myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myMissing"), theMissing));
myQueryRoot.setHasIndexJoins();
}
Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From<?, ? extends BaseResourceIndexedSearchParam> theFrom, Predicate thePredicate) {
Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From<?, ? extends BaseResourceIndexedSearchParam> theFrom, Predicate thePredicate, RequestPartitionId theRequestPartitionId) {
List<Predicate> andPredicates = new ArrayList<>();
addPartitionIdPredicate(theRequestPartitionId, 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(myPartitionSettings, theRequestPartitionId, theResourceName, theParamName);
Predicate hashIdentityPredicate = myCriteriaBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity);
andPredicates.add(hashIdentityPredicate);
andPredicates.add(thePredicate);
}
long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName);
Predicate hashIdentityPredicate = myCriteriaBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity);
return myCriteriaBuilder.and(hashIdentityPredicate, thePredicate);
return myCriteriaBuilder.and(toArray(andPredicates));
}
public PartitionSettings getPartitionSettings() {
return myPartitionSettings;
}
Predicate createPredicateNumeric(String theResourceName,
@ -155,7 +176,7 @@ abstract class BasePredicateBuilder {
ParamPrefixEnum thePrefix,
BigDecimal theValue,
final Expression<BigDecimal> thePath,
String invalidMessageName) {
String invalidMessageName, RequestPartitionId theRequestPartitionId) {
Predicate num;
// Per discussions with Grahame Grieve and James Agnew on 11/13/19, modified logic for EQUAL and NOT_EQUAL operators below so as to
// use exact value matching. The "fuzz amount" matching is still used with the APPROXIMATE operator.
@ -199,7 +220,20 @@ abstract class BasePredicateBuilder {
if (theParamName == null) {
return num;
}
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, num);
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, num, theRequestPartitionId);
}
void addPartitionIdPredicate(RequestPartitionId theRequestPartitionId, From<?, ? extends BasePartitionable> theJoin, List<Predicate> theCodePredicates) {
if (theRequestPartitionId != null) {
Integer partitionId = theRequestPartitionId.getPartitionId();
Predicate partitionPredicate;
if (partitionId != null) {
partitionPredicate = myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue").as(Integer.class), partitionId);
} else {
partitionPredicate = myCriteriaBuilder.isNull(theJoin.get("myPartitionIdValue").as(Integer.class));
}
myQueryRoot.addPredicate(partitionPredicate);
}
}
static String createLeftAndRightMatchLikeExpression(String likeExpression) {

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