Don't merge yet: MegaScale API Changes 2 (#4544)

* Megascale changes 2

* Cleanup

* CLeanup

* Sync master

* Cleanup

* Ongoing work

* Move service

* Fixes

* Ongoing megascaler work

* Work

* Work

* Fixes

* Work

* Test fixes

* Work on remote services

* Interceptor rework

* Work on config refactor

* Fixes

* Docs tweaks

* Address review comments

* Test tweak

* Test fixes

* Try to fix tests

* Test tweaks

* Fix changelogs

* Work on merging

* Changes

* Megascale fixes

* Remove accidental commit

* Address fixmes

* Cleanup

* Test fixes

* Compile fix

* Test fixes

* Account for review comments

* Bump HAPI FHIR version
This commit is contained in:
James Agnew 2023-02-27 22:19:28 -05:00 committed by GitHub
parent cedf69516b
commit bf495e2d92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
206 changed files with 2721 additions and 1378 deletions

View File

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

View File

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

View File

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

View File

@ -628,6 +628,13 @@ public enum Pointcut implements IPointcut {
* This method is called after all processing is completed for a request, but only if the
* request completes normally (i.e. no exception is thrown).
* <p>
* This pointcut is called after the response has completely finished, meaning that the HTTP respsonse to the client
* may or may not have already completely been returned to the client by the time this pointcut is invoked. Use caution
* if you have timing-dependent logic, since there is no guarantee about whether the client will have already moved on
* by the time your method is invoked. If you need a guarantee that your method is invoked before returning to the
* client, consider using {@link #SERVER_OUTGOING_RESPONSE} instead.
* </p>
* <p>
* Hooks may accept the following parameters:
* <ul>
* <li>
@ -1245,7 +1252,8 @@ public enum Pointcut implements IPointcut {
* <ul>
* <li>
* ca.uhn.fhir.rest.server.util.ICachedSearchDetails - Contains the details of the search that
* is being created and initialized
* is being created and initialized. Interceptors may use this parameter to modify aspects of the search
* before it is stored and executed.
* </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
@ -1264,6 +1272,9 @@ public enum Pointcut implements IPointcut {
* <li>
* ca.uhn.fhir.jpa.searchparam.SearchParameterMap - Contains the details of the search being checked. This can be modified.
* </li>
* <li>
* ca.uhn.fhir.interceptor.model.RequestPartitionId - The partition associated with the request (or {@literal null} if the server is not partitioned)
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
@ -1273,7 +1284,8 @@ public enum Pointcut implements IPointcut {
"ca.uhn.fhir.rest.server.util.ICachedSearchDetails",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.jpa.searchparam.SearchParameterMap"
"ca.uhn.fhir.jpa.searchparam.SearchParameterMap",
"ca.uhn.fhir.interceptor.model.RequestPartitionId"
),
/**
@ -1912,8 +1924,11 @@ public enum Pointcut implements IPointcut {
/**
* <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.
* Invoked before any FHIR read/access/extended operation (e.g. <b>read/vread</b>, <b>search</b>, <b>history</b>,
* <b>$reindex</b>, etc.) operation to request the identification of the partition ID to be associated with
* the resource(s) being searched for, read, etc. Essentially any operations in the JPA server that are not
* creating a resource will use this pointcut. Creates will use {@link #STORAGE_PARTITION_IDENTIFY_CREATE}.
*
* <p>
* This hook will only be called if
* partitioning is enabled in the JPA server.
@ -2193,6 +2208,30 @@ public enum Pointcut implements IPointcut {
"ca.uhn.fhir.rest.server.TransactionLogMessages",
"ca.uhn.fhir.mdm.api.MdmLinkEvent"),
/**
* <b>JPA Hook:</b>
* This hook is invoked when a cross-partition reference is about to be
* stored in the database.
* <p>
* <b>This is an experimental API - It may change in the future, use with caution.</b>
* </p>
* <p>
* Hooks may accept the following parameters:
* </p>
* <ul>
* <li>
* {@literal ca.uhn.fhir.jpa.searchparam.extractor.CrossPartitionReferenceDetails} - Contains details about the
* cross partition reference.
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
JPA_RESOLVE_CROSS_PARTITION_REFERENCE("ca.uhn.fhir.jpa.model.cross.IResourceLookup",
"ca.uhn.fhir.jpa.searchparam.extractor.CrossPartitionReferenceDetails"),
/**
* <b>Performance Tracing Hook:</b>
* This hook is invoked when any informational messages generated by the

View File

@ -137,12 +137,12 @@ public class RequestPartitionId implements IModelJson {
RequestPartitionId that = (RequestPartitionId) theO;
return new EqualsBuilder()
.append(myAllPartitions, that.myAllPartitions)
.append(myPartitionDate, that.myPartitionDate)
.append(myPartitionIds, that.myPartitionIds)
.append(myPartitionNames, that.myPartitionNames)
.isEquals();
EqualsBuilder b = new EqualsBuilder();
b.append(myAllPartitions, that.myAllPartitions);
b.append(myPartitionDate, that.myPartitionDate);
b.append(myPartitionIds, that.myPartitionIds);
b.append(myPartitionNames, that.myPartitionNames);
return b.isEquals();
}
@Override

View File

@ -307,6 +307,10 @@ public class Constants {
public static final String PARAMQUALIFIER_TOKEN_NOT_IN = ":not-in";
public static final String PARAMQUALIFIER_TOKEN_ABOVE = ":above";
public static final String PARAMQUALIFIER_TOKEN_BELOW = ":below";
/**
* The number of characters in a UUID (36)
*/
public static final int UUID_LENGTH = 36;
static {
CHARSET_UTF8 = StandardCharsets.UTF_8;

View File

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

View File

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

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.5.2-SNAPSHOT</version>
<version>6.5.3-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -105,8 +105,8 @@
<artifactId>mariadb-java-client</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -197,43 +197,49 @@ public class LoggingInterceptor implements IClientInterceptor {
/**
* Should a summary (one line) for each request be logged, containing the URL and other information
*/
public void setLogRequestBody(boolean theValue) {
public LoggingInterceptor setLogRequestBody(boolean theValue) {
myLogRequestBody = theValue;
return this;
}
/**
* Should headers for each request be logged, containing the URL and other information
*/
public void setLogRequestHeaders(boolean theValue) {
public LoggingInterceptor setLogRequestHeaders(boolean theValue) {
myLogRequestHeaders = theValue;
return this;
}
/**
* Should a summary (one line) for each request be logged, containing the URL and other information
*/
public void setLogRequestSummary(boolean theValue) {
public LoggingInterceptor setLogRequestSummary(boolean theValue) {
myLogRequestSummary = theValue;
return this;
}
/**
* Should a summary (one line) for each request be logged, containing the URL and other information
*/
public void setLogResponseBody(boolean theValue) {
public LoggingInterceptor setLogResponseBody(boolean theValue) {
myLogResponseBody = theValue;
return this;
}
/**
* Should headers for each request be logged, containing the URL and other information
*/
public void setLogResponseHeaders(boolean theValue) {
public LoggingInterceptor setLogResponseHeaders(boolean theValue) {
myLogResponseHeaders = theValue;
return this;
}
/**
* Should a summary (one line) for each request be logged, containing the URL and other information
*/
public void setLogResponseSummary(boolean theValue) {
public LoggingInterceptor setLogResponseSummary(boolean theValue) {
myLogResponseSummary = theValue;
return this;
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
---
type: add
issue: 4544
title: "A new API has been added to the JPA server for storing resources which
are actually placeholders for externally stored payloads. This API is not yet
used for any published use cases but will be rolled out for future uses."

View File

@ -0,0 +1,7 @@
---
type: add
issue: 4544
title: "The HapiTransactionService (HAPI FHIR JPA Transaction Manager) has been improved
and used in a number of new places (replacing the @Transactional annotation). This
means that stack traces have much less transaction logic noise, and give a clear
indication of transaction boundaries."

View File

@ -31,6 +31,7 @@ Creating your own interceptors is easy. Custom interceptor classes do not need t
* The method must have an appropriate return value for the chosen [Pointcut](/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html).
* The method may have any of the parameters specified for the given [Pointcut](/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html).
* A parameter of type [Pointcut](/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html) may also be added, and will be passed the actual Pointcut which triggered the invocation.
* The method must be public.

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
@ -53,6 +54,7 @@ import ca.uhn.fhir.jpa.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.delete.ThreadSafeResourceDeleterSvc;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceServiceRegistry;
import ca.uhn.fhir.jpa.graphql.DaoRegistryGraphQLStorageServices;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
@ -133,6 +135,7 @@ import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.jpa.term.api.ITermReindexingSvc;
import ca.uhn.fhir.jpa.term.config.TermCodeSystemConfig;
import ca.uhn.fhir.jpa.util.JpaHapiTransactionService;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.jpa.validation.ResourceLoaderImpl;
import ca.uhn.fhir.jpa.validation.ValidationSettings;
@ -227,6 +230,11 @@ public class JpaConfig {
return new CascadingDeleteInterceptor(theFhirContext, theDaoRegistry, theInterceptorBroadcaster, threadSafeResourceDeleterSvc);
}
@Bean
public ExternallyStoredResourceServiceRegistry ExternallyStoredResourceServiceRegistry() {
return new ExternallyStoredResourceServiceRegistry();
}
@Lazy
@Bean
public ThreadSafeResourceDeleterSvc safeDeleter(DaoRegistry theDaoRegistry, IInterceptorBroadcaster theInterceptorBroadcaster, HapiTransactionService hapiTransactionService) {
@ -383,7 +391,7 @@ public class JpaConfig {
@Bean
public HapiTransactionService hapiTransactionService() {
return new HapiTransactionService();
return new JpaHapiTransactionService();
}
@Bean
@ -524,8 +532,8 @@ public class JpaConfig {
@Bean(name = PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER)
@Scope("prototype")
public PersistedJpaSearchFirstPageBundleProvider newPersistedJpaSearchFirstPageBundleProvider(RequestDetails theRequest, Search theSearch, SearchTask theSearchTask, ISearchBuilder theSearchBuilder) {
return new PersistedJpaSearchFirstPageBundleProvider(theSearch, theSearchTask, theSearchBuilder, theRequest);
public PersistedJpaSearchFirstPageBundleProvider newPersistedJpaSearchFirstPageBundleProvider(RequestDetails theRequest, Search theSearch, SearchTask theSearchTask, ISearchBuilder theSearchBuilder, RequestPartitionId theRequestPartitionId) {
return new PersistedJpaSearchFirstPageBundleProvider(theSearch, theSearchTask, theSearchBuilder, theRequest, theRequestPartitionId);
}
@Bean(name = RepositoryValidatingRuleBuilder.REPOSITORY_VALIDATING_RULE_BUILDER)

View File

@ -30,6 +30,9 @@ import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.entity.PartitionEntity;
import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceAddress;
import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceAddressMetadataKey;
import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceServiceRegistry;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
@ -221,8 +224,14 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
@Autowired
protected InMemoryResourceMatcher myInMemoryResourceMatcher;
@Autowired
protected IJpaStorageResourceParser myJpaStorageResourceParser;
@Autowired
protected PartitionSettings myPartitionSettings;
@Autowired
ExpungeService myExpungeService;
@Autowired
private ExternallyStoredResourceServiceRegistry myExternallyStoredResourceServiceRegistry;
@Autowired
private ISearchParamPresenceSvc mySearchParamPresenceSvc;
@Autowired
private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor;
@ -231,18 +240,18 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
private FhirContext myContext;
private ApplicationContext myApplicationContext;
@Autowired
private PartitionSettings myPartitionSettings;
@Autowired
private IPartitionLookupSvc myPartitionLookupSvc;
@Autowired
private MemoryCacheService myMemoryCacheService;
@Autowired(required = false)
private IFulltextSearchSvc myFulltextSearchSvc;
@Autowired
private PlatformTransactionManager myTransactionManager;
@Autowired
protected IJpaStorageResourceParser myJpaStorageResourceParser;
@VisibleForTesting
public void setExternallyStoredResourceServiceRegistryForUnitTest(ExternallyStoredResourceServiceRegistry theExternallyStoredResourceServiceRegistry) {
myExternallyStoredResourceServiceRegistry = theExternallyStoredResourceServiceRegistry;
}
@VisibleForTesting
public void setSearchParamPresenceSvc(ISearchParamPresenceSvc theSearchParamPresenceSvc) {
@ -544,6 +553,20 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
if (thePerformIndexing) {
ExternallyStoredResourceAddress address = null;
if (myExternallyStoredResourceServiceRegistry.hasProviders()) {
address = ExternallyStoredResourceAddressMetadataKey.INSTANCE.get(theResource);
}
if (address != null) {
encoding = ResourceEncodingEnum.ESR;
resourceBinary = null;
resourceText = address.getProviderId() + ":" + address.getLocation();
changed = true;
} else {
encoding = myStorageSettings.getResourceEncoding();
String resourceType = theEntity.getResourceType();
@ -558,7 +581,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
HashFunction sha256 = Hashing.sha256();
HashCode hashCode;
String encodedResource = encodeResource(theResource, encoding, excludeElements, myContext);
if (getStorageSettings().getInlineResourceTextBelowSize() > 0 && encodedResource.length() < getStorageSettings().getInlineResourceTextBelowSize()) {
if (myStorageSettings.getInlineResourceTextBelowSize() > 0 && encodedResource.length() < myStorageSettings.getInlineResourceTextBelowSize()) {
resourceText = encodedResource;
resourceBinary = null;
encoding = ResourceEncodingEnum.JSON;
@ -582,6 +605,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
newSourceExtension.setValue(sourceExtension.getValue());
}
}
} else {
encoding = null;
@ -662,6 +687,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
break;
default:
case DEL:
case ESR:
resourceBinary = new byte[0];
break;
}
@ -866,8 +892,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
}
String toResourceName(IBaseResource theResource) {
return myContext.getResourceType(theResource);
}
@ -1579,6 +1603,14 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
myPartitionSettings = thePartitionSettings;
}
/**
* Do not call this method outside of unit tests
*/
@VisibleForTesting
public void setJpaStorageResourceParserForUnitTest(IJpaStorageResourceParser theJpaStorageResourceParser) {
myJpaStorageResourceParser = theJpaStorageResourceParser;
}
private class AddTagDefinitionToCacheAfterCommitSynchronization implements TransactionSynchronization {
private final TagDefinition myTagDefinition;
@ -1629,6 +1661,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
resourceText = GZipUtil.decompress(theResourceBytes);
break;
case DEL:
case ESR:
break;
}
return resourceText;
@ -1688,12 +1721,4 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest;
}
/**
* Do not call this method outside of unit tests
*/
@VisibleForTesting
public void setJpaStorageResourceParserForUnitTest(IJpaStorageResourceParser theJpaStorageResourceParser) {
myJpaStorageResourceParser = theJpaStorageResourceParser;
}
}

View File

@ -50,6 +50,7 @@ import ca.uhn.fhir.jpa.model.dao.JpaPid;
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.PartitionablePartitionId;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.TagDefinition;
@ -99,6 +100,7 @@ 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.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.util.ObjectUtil;
@ -147,6 +149,7 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -242,7 +245,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, @Nonnull TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) {
return myTransactionService.execute(theRequestDetails, theTransactionDetails, tx -> doCreateForPost(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails));
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequestDetails, theResource, getResourceName());
return myTransactionService
.withRequest(theRequestDetails)
.withTransactionDetails(theTransactionDetails)
.withRequestPartitionId(requestPartitionId)
.execute(tx -> doCreateForPost(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails, requestPartitionId));
}
@VisibleForTesting
@ -253,7 +261,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
/**
* Called for FHIR create (POST) operations
*/
protected DaoMethodOutcome doCreateForPost(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) {
protected DaoMethodOutcome doCreateForPost(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
if (theResource == null) {
String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "missingBody");
throw new InvalidRequestException(Msg.code(956) + msg);
@ -274,13 +282,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE);
}
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequestDetails, theResource, getResourceName());
return doCreateForPostOrPut(theRequestDetails, theResource, theIfNoneExist, true, thePerformIndexing, requestPartitionId, RestOperationTypeEnum.CREATE, theTransactionDetails);
return doCreateForPostOrPut(theRequestDetails, theResource, theIfNoneExist, true, thePerformIndexing, theRequestPartitionId, RestOperationTypeEnum.CREATE, theTransactionDetails);
}
/**
* Called both for FHIR create (POST) operations (via {@link #doCreateForPost(IBaseResource, String, boolean, TransactionDetails, RequestDetails)}
* as well as for FHIR update (PUT) where we're doing a create-with-client-assigned-ID (via {@link #doUpdate(IBaseResource, String, boolean, boolean, RequestDetails, TransactionDetails)}.
* Called both for FHIR create (POST) operations (via {@link #doCreateForPost(IBaseResource, String, boolean, TransactionDetails, RequestDetails, RequestPartitionId)}
* as well as for FHIR update (PUT) where we're doing a create-with-client-assigned-ID (via {@link #doUpdate(IBaseResource, String, boolean, boolean, RequestDetails, TransactionDetails, RequestPartitionId)}.
*/
private DaoMethodOutcome doCreateForPostOrPut(RequestDetails theRequest, T theResource, String theMatchUrl, boolean theProcessMatchUrl, boolean thePerformIndexing, RequestPartitionId theRequestPartitionId, RestOperationTypeEnum theOperationType, TransactionDetails theTransactionDetails) {
StopWatch w = new StopWatch();
@ -290,7 +297,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
ResourceTable entity = new ResourceTable();
entity.setResourceType(toResourceName(theResource));
entity.setPartitionId(myRequestPartitionHelperService.toStoragePartition(theRequestPartitionId));
entity.setPartitionId(PartitionablePartitionId.toStoragePartition(theRequestPartitionId, myPartitionSettings));
entity.setCreatedByMatchUrl(theMatchUrl);
entity.setVersion(1);
@ -895,10 +902,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
@Override
@Transactional
public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) {
StopWatch w = new StopWatch();
IBundleProvider retVal = myPersistedJpaBundleProviderFactory.history(theRequestDetails, myResourceName, null, theSince, theUntil, theOffset);
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(myResourceName, null);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, details);
IBundleProvider retVal = myTransactionService
.withRequest(theRequestDetails)
.withRequestPartitionId(requestPartitionId)
.execute(() -> {
return myPersistedJpaBundleProviderFactory.history(theRequestDetails, myResourceName, null, theSince, theUntil, theOffset, requestPartitionId);
});
ourLog.debug("Processed history on {} in {}ms", myResourceName, w.getMillisAndRestart());
return retVal;
}
@ -907,35 +920,48 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
* @deprecated Use {@link #history(IIdType, HistorySearchDateRangeParam, RequestDetails)} instead
*/
@Override
@Transactional
public IBundleProvider history(final IIdType theId, final Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequest) {
StopWatch w = new StopWatch();
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(myResourceName, theId);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, details);
IBundleProvider retVal = myTransactionService
.withRequest(theRequest)
.withRequestPartitionId(requestPartitionId)
.execute(() -> {
IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless();
BaseHasResource entity = readEntity(id, theRequest);
BaseHasResource entity = readEntity(id, true, theRequest, requestPartitionId);
IBundleProvider retVal = myPersistedJpaBundleProviderFactory.history(theRequest, myResourceName, entity.getId(), theSince, theUntil, theOffset);
return myPersistedJpaBundleProviderFactory.history(theRequest, myResourceName, entity.getId(), theSince, theUntil, theOffset, requestPartitionId);
});
ourLog.debug("Processed history on {} in {}ms", id, w.getMillisAndRestart());
ourLog.debug("Processed history on {} in {}ms", theId, w.getMillisAndRestart());
return retVal;
}
@Override
@Transactional
public IBundleProvider history(final IIdType theId, final HistorySearchDateRangeParam theHistorySearchDateRangeParam,
RequestDetails theRequest) {
StopWatch w = new StopWatch();
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(myResourceName, theId);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, details);
IBundleProvider retVal = myTransactionService
.withRequest(theRequest)
.withRequestPartitionId(requestPartitionId)
.execute(() -> {
IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless();
BaseHasResource entity = readEntity(id, theRequest);
BaseHasResource entity = readEntity(id, true, theRequest, requestPartitionId);
IBundleProvider retVal = myPersistedJpaBundleProviderFactory.history(theRequest, myResourceName, entity.getId(),
return myPersistedJpaBundleProviderFactory.history(theRequest, myResourceName, entity.getId(),
theHistorySearchDateRangeParam.getLowerBoundAsInstant(),
theHistorySearchDateRangeParam.getUpperBoundAsInstant(),
theHistorySearchDateRangeParam.getOffset(),
theHistorySearchDateRangeParam.getHistorySearchType());
theHistorySearchDateRangeParam.getHistorySearchType(),
requestPartitionId
);
});
ourLog.debug("Processed history on {} in {}ms", id, w.getMillisAndRestart());
ourLog.debug("Processed history on {} in {}ms", theId, w.getMillisAndRestart());
return retVal;
}
@ -962,8 +988,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
addAllResourcesTypesToReindex(theBase, theRequestDetails, params);
}
ReadPartitionIdRequestDetails details = new ReadPartitionIdRequestDetails(null, RestOperationTypeEnum.EXTENDED_OPERATION_SERVER, null, null, null);
RequestPartitionId requestPartition = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, null, details);
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forOperation(null, null, ProviderConstants.OPERATION_REINDEX);
RequestPartitionId requestPartition = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, details);
params.setRequestPartitionId(requestPartition);
JobInstanceStartRequest request = new JobInstanceStartRequest();
@ -1160,14 +1186,20 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
validateResourceTypeAndThrowInvalidRequestException(theId);
TransactionDetails transactionDetails = new TransactionDetails();
return myTransactionService.execute(theRequest, transactionDetails, tx -> doRead(theId, theRequest, theDeletedOk));
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(theRequest, myResourceName, theId);
return myTransactionService
.withRequest(theRequest)
.withTransactionDetails(transactionDetails)
.withRequestPartitionId(requestPartitionId)
.execute(() -> doReadInTransaction(theId, theRequest, theDeletedOk, requestPartitionId));
}
public T doRead(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) {
private T doReadInTransaction(IIdType theId, RequestDetails theRequest, boolean theDeletedOk, RequestPartitionId theRequestPartitionId) {
assert TransactionSynchronizationManager.isActualTransactionActive();
StopWatch w = new StopWatch();
BaseHasResource entity = readEntity(theId, theRequest);
BaseHasResource entity = readEntity(theId, true, theRequest, theRequestPartitionId);
validateResourceType(entity);
T retVal = myJpaStorageResourceParser.toResource(myResourceType, entity, null, false);
@ -1214,9 +1246,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
@Override
@Transactional
public BaseHasResource readEntity(IIdType theId, RequestDetails theRequest) {
return readEntity(theId, true, theRequest);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(theRequest, myResourceName, theId);
return myTransactionService
.withRequest(theRequest)
.withRequestPartitionId(requestPartitionId)
.execute(() -> readEntity(theId, true, theRequest, requestPartitionId));
}
@SuppressWarnings("unchecked")
@ -1245,13 +1280,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
}
@Override
@Transactional
public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest) {
private BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest, RequestPartitionId requestPartitionId) {
validateResourceTypeAndThrowInvalidRequestException(theId);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(theRequest, getResourceName(), theId);
BaseHasResource entity;
JpaPid pid = myIdHelperService.resolveResourcePersistentIds(requestPartitionId, getResourceName(), theId.getIdPart());
Set<Integer> readPartitions = null;
@ -1361,15 +1392,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
@Override
public void reindex(T theResource, ResourceTable theEntity) {
public void reindex(T theResource, IBasePersistedResource theEntity) {
assert TransactionSynchronizationManager.isActualTransactionActive();
ourLog.debug("Indexing resource {} - PID {}", theEntity.getIdDt().getValue(), theEntity.getId());
ourLog.debug("Indexing resource {} - PID {}", theEntity.getIdDt().getValue(), theEntity.getPersistentId());
if (theResource != null) {
CURRENTLY_REINDEXING.put(theResource, Boolean.TRUE);
}
TransactionDetails transactionDetails = new TransactionDetails(theEntity.getUpdatedDate());
ResourceTable entity = (ResourceTable) theEntity;
TransactionDetails transactionDetails = new TransactionDetails(entity.getUpdatedDate());
ResourceTable resourceTable = updateEntity(null, theResource, theEntity, theEntity.getDeleted(), true, false, transactionDetails, true, false);
if (theResource != null) {
CURRENTLY_REINDEXING.put(theResource, null);
@ -1450,6 +1482,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
if (retVal instanceof PersistedJpaBundleProvider) {
PersistedJpaBundleProvider provider = (PersistedJpaBundleProvider) retVal;
provider.setRequestPartitionId(requestPartitionId);
if (provider.getCacheStatus() == SearchCacheStatusEnum.HIT) {
if (theServletResponse != null && theRequest != null) {
String value = "HIT from " + theRequest.getFhirServerBase();
@ -1520,13 +1553,18 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public List<JpaPid> searchForIds(SearchParameterMap theParams, RequestDetails theRequest, @Nullable IBaseResource theConditionalOperationTargetOrNull) {
TransactionDetails transactionDetails = new TransactionDetails();
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, myResourceName, theParams, theConditionalOperationTargetOrNull);
return myTransactionService.execute(theRequest, transactionDetails, tx -> {
return myTransactionService
.withRequest(theRequest)
.withTransactionDetails(transactionDetails)
.withRequestPartitionId(requestPartitionId)
.execute(() -> {
if (theParams.getLoadSynchronousUpTo() != null) {
theParams.setLoadSynchronousUpTo(Math.min(getStorageSettings().getInternalSynchronousSearchSize(), theParams.getLoadSynchronousUpTo()));
theParams.setLoadSynchronousUpTo(Math.min(myStorageSettings.getInternalSynchronousSearchSize(), theParams.getLoadSynchronousUpTo()));
} else {
theParams.setLoadSynchronousUpTo(getStorageSettings().getInternalSynchronousSearchSize());
theParams.setLoadSynchronousUpTo(myStorageSettings.getInternalSynchronousSearchSize());
}
ISearchBuilder builder = mySearchBuilderFactory.newSearchBuilder(this, getResourceName(), getResourceType());
@ -1534,7 +1572,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
List<JpaPid> ids = new ArrayList<>();
String uuid = UUID.randomUUID().toString();
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams, theConditionalOperationTargetOrNull);
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
try (IResultIterator<JpaPid> iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) {
@ -1634,15 +1671,25 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
String id = theResource.getIdElement().getValue();
Runnable onRollback = () -> theResource.getIdElement().setValue(id);
// Execute the update in a retryable transaction
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource, getResourceName());
Callable<DaoMethodOutcome> updateCallback;
if (myStorageSettings.isUpdateWithHistoryRewriteEnabled() && theRequest != null && theRequest.isRewriteHistory()) {
return myTransactionService.execute(theRequest, theTransactionDetails, tx -> doUpdateWithHistoryRewrite(theResource, theRequest, theTransactionDetails), onRollback);
updateCallback = () -> doUpdateWithHistoryRewrite(theResource, theRequest, theTransactionDetails, requestPartitionId);
} else {
return myTransactionService.execute(theRequest, theTransactionDetails, tx -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails), onRollback);
}
updateCallback = () -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails, requestPartitionId);
}
private DaoMethodOutcome doUpdate(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
// Execute the update in a retryable transaction
return myTransactionService
.withRequest(theRequest)
.withTransactionDetails(theTransactionDetails)
.withRequestPartitionId(requestPartitionId)
.onRollback(onRollback)
.execute(updateCallback);
}
private DaoMethodOutcome doUpdate(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, TransactionDetails theTransactionDetails, RequestPartitionId theRequestPartitionId) {
T resource = theResource;
preProcessResourceForStorage(resource);
@ -1662,8 +1709,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
entity = myEntityManager.find(ResourceTable.class, pid.getId());
resourceId = entity.getIdDt();
} else {
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource, getResourceName());
DaoMethodOutcome outcome = doCreateForPostOrPut(theRequest, resource, theMatchUrl, false, thePerformIndexing, requestPartitionId, update, theTransactionDetails);
DaoMethodOutcome outcome = doCreateForPostOrPut(theRequest, resource, theMatchUrl, false, thePerformIndexing, theRequestPartitionId, update, theTransactionDetails);
// Pre-cache the match URL
if (outcome.getPersistentId() != null) {
@ -1682,8 +1728,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
assert resourceId != null;
assert resourceId.hasIdPart();
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource, getResourceName());
boolean create = false;
if (theRequest != null) {
@ -1695,14 +1739,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
if (!create) {
try {
entity = readEntityLatestVersion(resourceId, requestPartitionId, theTransactionDetails);
entity = readEntityLatestVersion(resourceId, theRequestPartitionId, theTransactionDetails);
} catch (ResourceNotFoundException e) {
create = true;
}
}
if (create) {
return doCreateForPostOrPut(theRequest, resource, null, false, thePerformIndexing, requestPartitionId, update, theTransactionDetails);
return doCreateForPostOrPut(theRequest, resource, null, false, thePerformIndexing, theRequestPartitionId, update, theTransactionDetails);
}
}
@ -1720,7 +1764,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
* @param theTransactionDetails details of the transaction
* @return the outcome of the operation
*/
private DaoMethodOutcome doUpdateWithHistoryRewrite(T theResource, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
private DaoMethodOutcome doUpdateWithHistoryRewrite(T theResource, RequestDetails theRequest, TransactionDetails theTransactionDetails, RequestPartitionId theRequestPartitionId) {
StopWatch w = new StopWatch();
// No need for indexing as this will update a non-current version of the resource which will not be searchable
@ -1736,7 +1780,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
assert resourceId.hasIdPart();
try {
currentEntity = readEntityLatestVersion(resourceId.toVersionless(), theRequest, theTransactionDetails);
currentEntity = readEntityLatestVersion(resourceId.toVersionless(), theRequestPartitionId, theTransactionDetails);
if (!resourceId.hasVersionIdPart()) {
throw new InvalidRequestException(Msg.code(2093) + "Invalid resource ID, ID must contain a history version");

View File

@ -3,6 +3,8 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
@ -10,9 +12,12 @@ import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
import ca.uhn.fhir.jpa.dao.expunge.ExpungeService;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory;
import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
import ca.uhn.fhir.jpa.util.QueryChunker;
@ -88,6 +93,10 @@ public abstract class BaseHapiFhirSystemDao<T extends IBaseBundle, MT> extends B
private IResourceTagDao myResourceTagDao;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
private IRequestPartitionHelperSvc myRequestPartitionHelperService;
@Autowired
private IHapiTransactionService myTransactionService;
@VisibleForTesting
public void setTransactionProcessorForUnitTest(TransactionProcessor theTransactionProcessor) {
@ -124,7 +133,6 @@ public abstract class BaseHapiFhirSystemDao<T extends IBaseBundle, MT> extends B
return retVal;
}
@Transactional(propagation = Propagation.SUPPORTS)
@Nullable
@Override
public Map<String, Long> getResourceCountsFromCache() {
@ -138,26 +146,31 @@ public abstract class BaseHapiFhirSystemDao<T extends IBaseBundle, MT> extends B
@Override
public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) {
StopWatch w = new StopWatch();
IBundleProvider retVal = myPersistedJpaBundleProviderFactory.history(theRequestDetails, null, null, theSince, theUntil, theOffset);
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(null, null);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, details);
IBundleProvider retVal = myTransactionService
.withRequest(theRequestDetails)
.withRequestPartitionId(requestPartitionId)
.execute(() -> myPersistedJpaBundleProviderFactory.history(theRequestDetails, null, null, theSince, theUntil, theOffset, requestPartitionId));
ourLog.info("Processed global history in {}ms", w.getMillisAndRestart());
return retVal;
}
@Override
@Transactional(propagation = Propagation.NEVER)
public T transaction(RequestDetails theRequestDetails, T theRequest) {
HapiTransactionService.noTransactionAllowed();
return myTransactionProcessor.transaction(theRequestDetails, theRequest, false);
}
@Override
@Transactional(propagation = Propagation.MANDATORY)
public T transactionNested(RequestDetails theRequestDetails, T theRequest) {
HapiTransactionService.requireTransaction();
return myTransactionProcessor.transaction(theRequestDetails, theRequest, true);
}
@Override
@Transactional(propagation = Propagation.MANDATORY)
public <P extends IResourcePersistentId> void preFetchResources(List<P> theResolvedIds) {
HapiTransactionService.requireTransaction();
List<Long> pids = theResolvedIds
.stream()
.map(t -> ((JpaPid) t).getId())

View File

@ -29,6 +29,8 @@ import ca.uhn.fhir.jpa.api.dao.IDao;
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
import ca.uhn.fhir.jpa.entity.PartitionEntity;
import ca.uhn.fhir.jpa.entity.ResourceSearchView;
import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceServiceRegistry;
import ca.uhn.fhir.jpa.esr.IExternallyStoredResourceService;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.entity.BaseTag;
@ -87,6 +89,8 @@ public class JpaStorageResourceParser implements IJpaStorageResourceParser {
private PartitionSettings myPartitionSettings;
@Autowired
private IPartitionLookupSvc myPartitionLookupSvc;
@Autowired
private ExternallyStoredResourceServiceRegistry myExternallyStoredResourceServiceRegistry;
@Override
public IBaseResource toResource(IBasePersistedResource theEntity, boolean theForHistoryOperation) {
@ -231,18 +235,29 @@ public class JpaStorageResourceParser implements IJpaStorageResourceParser {
}
@SuppressWarnings("unchecked")
private <R extends IBaseResource> R parseResource(IBaseResourceEntity theEntity, ResourceEncodingEnum resourceEncoding, String decodedResourceText, Class<R> resourceType) {
private <R extends IBaseResource> R parseResource(IBaseResourceEntity theEntity, ResourceEncodingEnum theResourceEncoding, String theDecodedResourceText, Class<R> theResourceType) {
R retVal;
if (resourceEncoding != ResourceEncodingEnum.DEL) {
if (theResourceEncoding == ResourceEncodingEnum.ESR) {
int colonIndex = theDecodedResourceText.indexOf(':');
Validate.isTrue(colonIndex > 0, "Invalid ESR address: %s", theDecodedResourceText);
String providerId = theDecodedResourceText.substring(0, colonIndex);
String address = theDecodedResourceText.substring(colonIndex + 1);
Validate.notBlank(providerId, "No provider ID in ESR address: %s", theDecodedResourceText);
Validate.notBlank(address, "No address in ESR address: %s", theDecodedResourceText);
IExternallyStoredResourceService provider = myExternallyStoredResourceServiceRegistry.getProvider(providerId);
retVal = (R) provider.fetchResource(address);
} else if (theResourceEncoding != ResourceEncodingEnum.DEL) {
IParser parser = new TolerantJsonParser(getContext(theEntity.getFhirVersion()), LENIENT_ERROR_HANDLER, theEntity.getId());
try {
retVal = parser.parseResource(resourceType, decodedResourceText);
retVal = parser.parseResource(theResourceType, theDecodedResourceText);
} catch (Exception e) {
StringBuilder b = new StringBuilder();
b.append("Failed to parse database resource[");
b.append(myFhirContext.getResourceType(resourceType));
b.append(myFhirContext.getResourceType(theResourceType));
b.append("/");
b.append(theEntity.getIdDt().getIdPart());
b.append(" (pid ");

View File

@ -23,6 +23,8 @@ package ca.uhn.fhir.jpa.dao.expunge;
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.ReadPartitionIdRequestDetails;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity;
import ca.uhn.fhir.jpa.entity.Batch2WorkChunkEntity;
@ -69,8 +71,10 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTag;
import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity;
import ca.uhn.fhir.jpa.model.entity.TagDefinition;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.util.StopWatch;
@ -96,12 +100,13 @@ public class ExpungeEverythingService implements IExpungeEverythingService {
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
private HapiTransactionService myTxService;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
private MemoryCacheService myMemoryCacheService;
@Autowired
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
private int deletedResourceEntityCount;
@ -118,63 +123,67 @@ public class ExpungeEverythingService implements IExpungeEverythingService {
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING, hooks);
ourLog.info("BEGINNING GLOBAL $expunge");
myTxService.withRequest(theRequest).withPropagation(Propagation.REQUIRES_NEW).execute(()-> {
Propagation propagation = Propagation.REQUIRES_NEW;
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forOperation(null, null, ProviderConstants.OPERATION_EXPUNGE);
RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequest, details);
myTxService.withRequest(theRequest).withPropagation(propagation).withRequestPartitionId(requestPartitionId).execute(() -> {
counter.addAndGet(doExpungeEverythingQuery("UPDATE " + TermCodeSystem.class.getSimpleName() + " d SET d.myCurrentVersion = null"));
});
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, Batch2WorkChunkEntity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, Batch2JobInstanceEntity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageVersionResourceEntity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageVersionEntity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageEntity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchParamPresentEntity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobFileEntity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobEntity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ForcedId.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamDate.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamNumber.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamQuantity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamQuantityNormalized.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamString.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamToken.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamUri.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamCoords.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedComboStringUnique.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedComboTokenNonUnique.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceLink.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchResult.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchInclude.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermValueSetConceptDesignation.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermValueSetConcept.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermValueSet.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptParentChildLink.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMapGroupElementTarget.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMapGroupElement.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMapGroup.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMap.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptProperty.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptDesignation.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConcept.class));
myTxService.withRequest(theRequest).withPropagation(Propagation.REQUIRES_NEW).execute(()-> {
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, Batch2WorkChunkEntity.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, Batch2JobInstanceEntity.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageVersionResourceEntity.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageVersionEntity.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageEntity.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchParamPresentEntity.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobFileEntity.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobEntity.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ForcedId.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamDate.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamNumber.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamQuantity.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamQuantityNormalized.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamString.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamToken.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamUri.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedSearchParamCoords.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedComboStringUnique.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceIndexedComboTokenNonUnique.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceLink.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchResult.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchInclude.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermValueSetConceptDesignation.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermValueSetConcept.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermValueSet.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptParentChildLink.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMapGroupElementTarget.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMapGroupElement.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMapGroup.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMap.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptProperty.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptDesignation.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConcept.class, requestPartitionId));
myTxService.withRequest(theRequest).withPropagation(propagation).withRequestPartitionId(requestPartitionId).execute(() -> {
for (TermCodeSystem next : myEntityManager.createQuery("SELECT c FROM " + TermCodeSystem.class.getName() + " c", TermCodeSystem.class).getResultList()) {
next.setCurrentVersion(null);
myEntityManager.merge(next);
}
});
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystemVersion.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystem.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SubscriptionTable.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTag.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTag.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TagDefinition.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryProvenanceEntity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTable.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystemVersion.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystem.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SubscriptionTable.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTag.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTag.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TagDefinition.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryProvenanceEntity.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTable.class, requestPartitionId));
int counterBefore = counter.get();
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTable.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, PartitionEntity.class));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTable.class, requestPartitionId));
counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, PartitionEntity.class, requestPartitionId));
deletedResourceEntityCount = counter.get() - counterBefore;
myTxService.withRequest(theRequest).withPropagation(Propagation.REQUIRES_NEW).execute(()-> {
myTxService.withRequest(theRequest).withPropagation(propagation).withRequestPartitionId(requestPartitionId).execute(() -> {
counter.addAndGet(doExpungeEverythingQuery("DELETE from " + Search.class.getSimpleName() + " d"));
});
@ -192,12 +201,12 @@ public class ExpungeEverythingService implements IExpungeEverythingService {
myMemoryCacheService.invalidateAllCaches();
}
private int expungeEverythingByTypeWithoutPurging(RequestDetails theRequest, Class<?> theEntityType) {
private int expungeEverythingByTypeWithoutPurging(RequestDetails theRequest, Class<?> theEntityType, RequestPartitionId theRequestPartitionId) {
int outcome = 0;
while (true) {
StopWatch sw = new StopWatch();
int count = myTxService.withRequest(theRequest).withPropagation(Propagation.REQUIRES_NEW).execute(()-> {
int count = myTxService.withRequest(theRequest).withPropagation(Propagation.REQUIRES_NEW).withRequestPartitionId(theRequestPartitionId).execute(() -> {
CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();
CriteriaQuery<?> cq = cb.createQuery(theEntityType);
cq.from(theEntityType);
@ -215,14 +224,14 @@ public class ExpungeEverythingService implements IExpungeEverythingService {
break;
}
ourLog.info("Have deleted {} entities of type {} in {}", outcome, theEntityType.getSimpleName(), sw.toString());
ourLog.info("Have deleted {} entities of type {} in {}", outcome, theEntityType.getSimpleName(), sw);
}
return outcome;
}
@Override
public int expungeEverythingByType(Class<?> theEntityType) {
int result = expungeEverythingByTypeWithoutPurging(null, theEntityType);
int result = expungeEverythingByTypeWithoutPurging(null, theEntityType, RequestPartitionId.allPartitions());
purgeAllCaches();
return result;
}
@ -235,7 +244,7 @@ public class ExpungeEverythingService implements IExpungeEverythingService {
private int doExpungeEverythingQuery(String theQuery) {
StopWatch sw = new StopWatch();
int outcome = myEntityManager.createQuery(theQuery).executeUpdate();
ourLog.debug("SqlQuery affected {} rows in {}: {}", outcome, sw.toString(), theQuery);
ourLog.debug("SqlQuery affected {} rows in {}: {}", outcome, sw, theQuery);
return outcome;
}
}

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.entity;
*/
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
import ca.uhn.fhir.rest.api.Constants;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.r5.model.InstantType;
@ -46,6 +47,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import static ca.uhn.fhir.rest.api.Constants.UUID_LENGTH;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.left;
@ -58,7 +60,7 @@ import static org.apache.commons.lang3.StringUtils.left;
* See the BulkExportAppCtx for job details
*/
@Entity
@Table(name = "HFJ_BLK_EXPORT_JOB", uniqueConstraints = {
@Table(name = BulkExportJobEntity.HFJ_BLK_EXPORT_JOB, uniqueConstraints = {
@UniqueConstraint(name = "IDX_BLKEX_JOB_ID", columnNames = "JOB_ID")
}, indexes = {
@Index(name = "IDX_BLKEX_EXPTIME", columnList = "EXP_TIME")
@ -68,13 +70,15 @@ public class BulkExportJobEntity implements Serializable {
public static final int REQUEST_LENGTH = 1024;
public static final int STATUS_MESSAGE_LEN = 500;
public static final String JOB_ID = "JOB_ID";
public static final String HFJ_BLK_EXPORT_JOB = "HFJ_BLK_EXPORT_JOB";
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_BLKEXJOB_PID")
@SequenceGenerator(name = "SEQ_BLKEXJOB_PID", sequenceName = "SEQ_BLKEXJOB_PID")
@Column(name = "PID")
private Long myId;
@Column(name = "JOB_ID", length = Search.UUID_COLUMN_LENGTH, nullable = false)
@Column(name = JOB_ID, length = UUID_LENGTH, nullable = false)
private String myJobId;
@Enumerated(EnumType.STRING)

View File

@ -40,21 +40,24 @@ import javax.persistence.Version;
import java.io.Serializable;
import java.util.Date;
import static ca.uhn.fhir.rest.api.Constants.UUID_LENGTH;
import static org.apache.commons.lang3.StringUtils.left;
@Entity
@Table(name = "HFJ_BLK_IMPORT_JOB", uniqueConstraints = {
@Table(name = BulkImportJobEntity.HFJ_BLK_IMPORT_JOB, uniqueConstraints = {
@UniqueConstraint(name = "IDX_BLKIM_JOB_ID", columnNames = "JOB_ID")
})
public class BulkImportJobEntity implements Serializable {
public static final String HFJ_BLK_IMPORT_JOB = "HFJ_BLK_IMPORT_JOB";
public static final String JOB_ID = "JOB_ID";
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_BLKIMJOB_PID")
@SequenceGenerator(name = "SEQ_BLKIMJOB_PID", sequenceName = "SEQ_BLKIMJOB_PID")
@Column(name = "PID")
private Long myId;
@Column(name = "JOB_ID", length = Search.UUID_COLUMN_LENGTH, nullable = false, updatable = false)
@Column(name = JOB_ID, length = UUID_LENGTH, nullable = false, updatable = false)
private String myJobId;
@Column(name = "JOB_DESC", nullable = true, length = BulkExportJobEntity.STATUS_MESSAGE_LEN)
private String myJobDescription;

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.rest.api.Constants;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.r4.model.InstantType;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -149,8 +150,8 @@ public class ResourceReindexJobEntity implements Serializable {
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("id", myId)
.append("resourceType", myResourceType)
.append("thresholdLow", myThresholdLow)
.append("thresholdHigh", myThresholdHigh);
.append("thresholdLow", new InstantType(myThresholdLow))
.append("thresholdHigh", new InstantType(myThresholdHigh));
if (myDeleted) {
b.append("deleted", myDeleted);
}

View File

@ -76,13 +76,18 @@ import static org.apache.commons.lang3.StringUtils.left;
})
public class Search implements ICachedSearchDetails, Serializable {
/**
* Long enough to accommodate a full UUID (36) with an additional prefix
* used by megascale (12)
*/
@SuppressWarnings("WeakerAccess")
public static final int UUID_COLUMN_LENGTH = 36;
public static final int SEARCH_UUID_COLUMN_LENGTH = 48;
public static final String HFJ_SEARCH = "HFJ_SEARCH";
private static final int MAX_SEARCH_QUERY_STRING = 10000;
private static final int FAILURE_MESSAGE_LENGTH = 500;
private static final long serialVersionUID = 1L;
private static final Logger ourLog = LoggerFactory.getLogger(Search.class);
public static final String SEARCH_UUID = "SEARCH_UUID";
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "CREATED", nullable = false, updatable = false)
private Date myCreated;
@ -136,7 +141,7 @@ public class Search implements ICachedSearchDetails, Serializable {
private SearchStatusEnum myStatus;
@Column(name = "TOTAL_COUNT", nullable = true)
private Integer myTotalCount;
@Column(name = "SEARCH_UUID", length = UUID_COLUMN_LENGTH, nullable = false, updatable = false)
@Column(name = SEARCH_UUID, length = SEARCH_UUID_COLUMN_LENGTH, nullable = false, updatable = false)
private String myUuid;
@SuppressWarnings("unused")
@Version
@ -146,6 +151,9 @@ public class Search implements ICachedSearchDetails, Serializable {
@Column(name = "SEARCH_PARAM_MAP", nullable = true)
private byte[] mySearchParameterMap;
@Transient
private transient SearchParameterMap mySearchParameterMapTransient;
/**
* This isn't currently persisted in the DB as it's only used for offset mode. We could
* change this if needed in the future.
@ -363,10 +371,12 @@ public class Search implements ICachedSearchDetails, Serializable {
myTotalCount = theTotalCount;
}
@Override
public String getUuid() {
return myUuid;
}
@Override
public void setUuid(String theUuid) {
myUuid = theUuid;
}
@ -402,11 +412,26 @@ public class Search implements ICachedSearchDetails, Serializable {
return myVersion;
}
/**
* Note that this is not always set! We set this if we're storing a
* Search in {@link SearchStatusEnum#PASSCMPLET} status since we'll need
* the map in order to restart, but otherwise we save space and time by
* not storing it.
*/
public Optional<SearchParameterMap> getSearchParameterMap() {
return Optional.ofNullable(mySearchParameterMap).map(t -> SerializationUtils.deserialize(mySearchParameterMap));
if (mySearchParameterMapTransient != null) {
return Optional.of(mySearchParameterMapTransient);
}
SearchParameterMap searchParameterMap = null;
if (mySearchParameterMap != null) {
searchParameterMap = SerializationUtils.deserialize(mySearchParameterMap);
mySearchParameterMapTransient = searchParameterMap;
}
return Optional.ofNullable(searchParameterMap);
}
public void setSearchParameterMap(SearchParameterMap theSearchParameterMap) {
mySearchParameterMapTransient = theSearchParameterMap;
mySearchParameterMap = SerializationUtils.serialize(theSearchParameterMap);
}

View File

@ -0,0 +1,52 @@
package ca.uhn.fhir.jpa.esr;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public class ExternallyStoredResourceAddress {
private final String myProviderId;
private final String myLocation;
/**
* Constructor
*
* @param theProviderId The ID of the provider which will handle this address. Must match the ID returned by a registered {@link IExternallyStoredResourceService}.
* @param theLocation The actual location for the provider to resolve. The format of this string is entirely up to the {@link IExternallyStoredResourceService} and only needs to make sense to it.
*/
public ExternallyStoredResourceAddress(String theProviderId, String theLocation) {
myProviderId = theProviderId;
myLocation = theLocation;
}
/**
* @return The ID of the provider which will handle this address. Must match the ID returned by a registered {@link IExternallyStoredResourceService}.
*/
public String getProviderId() {
return myProviderId;
}
/**
* @return The actual location for the provider to resolve. The format of this string is entirely up to the {@link IExternallyStoredResourceService} and only needs to make sense to it.
*/
public String getLocation() {
return myLocation;
}
}

View File

@ -0,0 +1,39 @@
package ca.uhn.fhir.jpa.esr;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
public class ExternallyStoredResourceAddressMetadataKey extends ResourceMetadataKeyEnum<ExternallyStoredResourceAddress> {
/**
* Singleton instance
*/
public static final ExternallyStoredResourceAddressMetadataKey INSTANCE = new ExternallyStoredResourceAddressMetadataKey();
/**
* Constructor
*/
private ExternallyStoredResourceAddressMetadataKey() {
super("ExternallyStoredResourceAddress", ExternallyStoredResourceAddress.class);
}
}

View File

@ -0,0 +1,69 @@
package ca.uhn.fhir.jpa.esr;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.apache.commons.lang3.Validate;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;
import static org.apache.commons.lang3.StringUtils.defaultString;
public class ExternallyStoredResourceServiceRegistry {
private static final String VALID_ID_PATTERN = "[a-zA-Z0-9_.-]+";
private final Map<String, IExternallyStoredResourceService> myIdToProvider = new HashMap<>();
/**
* Registers a new provider. Do not call this method after the server has been started.
*
* @param theProvider The provider to register.
*/
public void registerProvider(@Nonnull IExternallyStoredResourceService theProvider) {
String id = defaultString(theProvider.getId());
Validate.isTrue(id.matches(VALID_ID_PATTERN), "Invalid provider ID (must match pattern " + VALID_ID_PATTERN + "): %s", id);
Validate.isTrue(!myIdToProvider.containsKey(id), "Already have a provider with ID: %s", id);
myIdToProvider.put(id, theProvider);
}
/**
* Do we have any providers registered?
*/
public boolean hasProviders() {
return !myIdToProvider.isEmpty();
}
/**
* Clears all registered providers. This method is mostly intended for unit tests.
*/
public void clearProviders() {
myIdToProvider.clear();
}
@Nonnull
public IExternallyStoredResourceService getProvider(@Nonnull String theProviderId) {
IExternallyStoredResourceService retVal = myIdToProvider.get(theProviderId);
Validate.notNull(retVal, "Invalid ESR provider ID: %s", theProviderId);
return retVal;
}
}

View File

@ -0,0 +1,41 @@
package ca.uhn.fhir.jpa.esr;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface IExternallyStoredResourceService {
/**
* Returns the ID of this provider. No two providers may return the same
* ID, and this provider should always return the same ID.
*/
String getId();
/**
* Fetches the given resource using the given address string
*
* @param theAddress The address string is a format that is entirely up to the individual provider. HAPI FHIR
* doesn't try to understand it.
* @return HAPI FHIR may modify the returned object, so it is important to always return a new object for every call here (careful with caching!)
*/
IBaseResource fetchResource(String theAddress);
}

View File

@ -0,0 +1,34 @@
/**
* This package stores the mechanism for Externally Stored Resources (ESRs).
* <p>
* A normal resource stores its contents in the {@link ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable}
* entity as text (compressed JSON or otherwise) with one row in that table per version of the
* resource. An ESR still creates a row in that table, but stores a reference to actual body
* storage instead.
* <p>
* THIS IS AN EXPERIMENTAL API - IT MAY GO AWAY OR CHANGE IN THE FUTURE
*
* @since 6.6.0
*/
package ca.uhn.fhir.jpa.esr;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/

View File

@ -21,6 +21,8 @@ package ca.uhn.fhir.jpa.migrate.tasks;
*/
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.entity.BulkExportJobEntity;
import ca.uhn.fhir.jpa.entity.BulkImportJobEntity;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
import ca.uhn.fhir.jpa.migrate.taskdef.ArbitrarySqlTask;
@ -31,13 +33,13 @@ import ca.uhn.fhir.jpa.migrate.tasks.api.BaseMigrationTasks;
import ca.uhn.fhir.jpa.migrate.tasks.api.Builder;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.util.VersionEnum;
import java.util.Arrays;
@ -48,6 +50,8 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static ca.uhn.fhir.rest.api.Constants.UUID_LENGTH;
@SuppressWarnings({"SqlNoDataSourceInspection", "SpellCheckingInspection"})
public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
@ -86,15 +90,13 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
init600(); // 20211102 -
init610();
init620();
init630();
init640();
init660();
}
protected void init660() {
Builder version = forVersion(VersionEnum.V6_6_0);
// fix Postgres clob types - that stupid oid driver problem is still there
// BT2_JOB_INSTANCE.PARAMS_JSON_LOB
version.onTable("BT2_JOB_INSTANCE")
@ -105,12 +107,25 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
// BT2_WORK_CHUNK.CHUNK_DATA
version.onTable("BT2_WORK_CHUNK")
.migratePostgresTextClobToBinaryClob("20230208.3", "CHUNK_DATA");
version
.onTable(Search.HFJ_SEARCH)
.addColumn("20230215.1", Search.SEARCH_UUID)
.nullable()
.type(ColumnTypeEnum.STRING, Search.SEARCH_UUID_COLUMN_LENGTH);
version
.onTable(BulkImportJobEntity.HFJ_BLK_IMPORT_JOB)
.addColumn("20230215.2", BulkImportJobEntity.JOB_ID)
.nullable()
.type(ColumnTypeEnum.STRING, UUID_LENGTH);
version
.onTable(BulkExportJobEntity.HFJ_BLK_EXPORT_JOB)
.addColumn("20230215.3", BulkExportJobEntity.JOB_ID)
.nullable()
.type(ColumnTypeEnum.STRING, UUID_LENGTH);
}
protected void init640() {
}
protected void init630() {
Builder version = forVersion(VersionEnum.V6_3_0);
// start forced_id inline migration
@ -822,7 +837,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
// Bulk Import Job
Builder.BuilderAddTableByColumns blkImportJobTable = version.addTableByColumns("20210410.1", "HFJ_BLK_IMPORT_JOB", "PID");
blkImportJobTable.addColumn("PID").nonNullable().type(ColumnTypeEnum.LONG);
blkImportJobTable.addColumn("JOB_ID").nonNullable().type(ColumnTypeEnum.STRING, Search.UUID_COLUMN_LENGTH);
blkImportJobTable.addColumn("JOB_ID").nonNullable().type(ColumnTypeEnum.STRING, UUID_LENGTH);
blkImportJobTable.addColumn("JOB_STATUS").nonNullable().type(ColumnTypeEnum.STRING, 10);
blkImportJobTable.addColumn("STATUS_TIME").nonNullable().type(ColumnTypeEnum.DATE_TIMESTAMP);
blkImportJobTable.addColumn("STATUS_MESSAGE").nullable().type(ColumnTypeEnum.STRING, 500);
@ -1500,7 +1515,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
version.onTable("HFJ_SEARCH")
.addColumn("20190814.6", "SEARCH_PARAM_MAP").nullable().type(ColumnTypeEnum.BLOB);
version.onTable("HFJ_SEARCH")
.modifyColumn("20190814.7", "SEARCH_UUID").nonNullable().withType(ColumnTypeEnum.STRING, 36);
.modifyColumn("20190814.7", "SEARCH_UUID").nonNullable().withType(ColumnTypeEnum.STRING, Search.SEARCH_UUID_COLUMN_LENGTH);
version.onTable("HFJ_SEARCH_PARM").dropThisTable("20190814.8");

View File

@ -28,6 +28,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.BasePagingProvider;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nullable;
// Note: this class is not annotated with @Service because we want to
// explicitly define it in BaseConfig.java. This is done so that
// implementors can override if they want to.
@ -51,6 +53,7 @@ public class DatabaseBackedPagingProvider extends BasePagingProvider {
/**
* Constructor
*
* @deprecated Use {@link DatabaseBackedPagingProvider} as this constructor has no purpose
*/
@Deprecated
@ -60,12 +63,19 @@ public class DatabaseBackedPagingProvider extends BasePagingProvider {
@Override
public synchronized IBundleProvider retrieveResultList(RequestDetails theRequestDetails, String theId) {
myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequestDetails, "Bundle", null, null);
PersistedJpaBundleProvider provider = myPersistedJpaBundleProviderFactory.newInstance(theRequestDetails, theId);
if (!provider.ensureSearchEntityLoaded()) {
return validateAndReturnBundleProvider(provider);
}
/**
* Subclasses may override in order to modify the bundle provider being returned
*/
@Nullable
protected PersistedJpaBundleProvider validateAndReturnBundleProvider(PersistedJpaBundleProvider theBundleProvider) {
if (!theBundleProvider.ensureSearchEntityLoaded()) {
return null;
}
return provider;
return theBundleProvider;
}
@Override

View File

@ -24,6 +24,7 @@ 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.ReadPartitionIdRequestDetails;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
@ -40,9 +41,10 @@ import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.model.primitive.InstantDt;
@ -98,7 +100,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
@Autowired
private ISearchCacheSvc mySearchCacheSvc;
@Autowired
private RequestPartitionHelperSvc myRequestPartitionHelperSvc;
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
@Autowired
private JpaStorageSettings myStorageSettings;
@Autowired
@ -130,6 +132,11 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
myUuid = theSearch.getUuid();
}
@VisibleForTesting
public void setRequestPartitionHelperSvcForUnitTest(IRequestPartitionHelperSvc theRequestPartitionHelperSvc) {
myRequestPartitionHelperSvc = theRequestPartitionHelperSvc;
}
protected Search getSearchEntity() {
return mySearchEntity;
}
@ -148,7 +155,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder(mySearchEntity.getResourceType(),
mySearchEntity.getResourceId(), mySearchEntity.getLastUpdatedLow(), mySearchEntity.getLastUpdatedHigh());
RequestPartitionId partitionId = getRequestPartitionIdForHistory();
RequestPartitionId partitionId = getRequestPartitionId();
List<ResourceHistoryTable> results = historyBuilder.fetchEntities(partitionId, theOffset, theFromIndex,
theToIndex, mySearchEntity.getHistorySearchStyle());
@ -194,18 +201,26 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
}
@Nonnull
private RequestPartitionId getRequestPartitionIdForHistory() {
protected final RequestPartitionId getRequestPartitionId() {
if (myRequestPartitionId == null) {
if (mySearchEntity.getResourceId() != null) {
// If we have an ID, we've already checked the partition and made sure it's appropriate
myRequestPartitionId = RequestPartitionId.allPartitions();
ReadPartitionIdRequestDetails details;
if (mySearchEntity == null) {
details = ReadPartitionIdRequestDetails.forSearchUuid(myUuid);
} else if (mySearchEntity.getSearchType() == SearchTypeEnum.HISTORY) {
details = ReadPartitionIdRequestDetails.forHistory(mySearchEntity.getResourceType(), null);
} else {
myRequestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(myRequest, mySearchEntity.getResourceType(), null);
SearchParameterMap params = mySearchEntity.getSearchParameterMap().orElse(null);
details = ReadPartitionIdRequestDetails.forSearchType(mySearchEntity.getResourceType(), params, null);
}
myRequestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(myRequest, details);
}
return myRequestPartitionId;
}
public void setRequestPartitionId(RequestPartitionId theRequestPartitionId) {
myRequestPartitionId = theRequestPartitionId;
}
protected List<IBaseResource> doSearchOrEverything(final int theFromIndex, final int theToIndex) {
if (mySearchEntity.getTotalCount() != null && mySearchEntity.getNumFound() <= 0) {
// No resources to fetch (e.g. we did a _summary=count search)
@ -217,8 +232,14 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(dao, resourceName, resourceType);
final List<JpaPid> pidsSubList = mySearchCoordinatorSvc.getResources(myUuid, theFromIndex, theToIndex, myRequest);
return myTxService.withRequest(myRequest).execute(() -> toResourceList(sb, pidsSubList));
RequestPartitionId requestPartitionId = getRequestPartitionId();
final List<JpaPid> pidsSubList = mySearchCoordinatorSvc.getResources(myUuid, theFromIndex, theToIndex, myRequest, requestPartitionId);
return myTxService
.withRequest(myRequest)
.withRequestPartitionId(requestPartitionId)
.execute(() -> {
return toResourceList(sb, pidsSubList);
});
}
/**
@ -226,7 +247,10 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
*/
public boolean ensureSearchEntityLoaded() {
if (mySearchEntity == null) {
Optional<Search> searchOpt = myTxService.withRequest(myRequest).execute(() -> mySearchCacheSvc.fetchByUuid(myUuid));
Optional<Search> searchOpt = myTxService
.withRequest(myRequest)
.withRequestPartitionId(myRequestPartitionId)
.execute(() -> mySearchCacheSvc.fetchByUuid(myUuid));
if (!searchOpt.isPresent()) {
return false;
}
@ -263,9 +287,12 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
key = MemoryCacheService.HistoryCountKey.forSystem();
}
Function<MemoryCacheService.HistoryCountKey, Integer> supplier = k -> myTxService.withRequest(myRequest).execute(() -> {
Function<MemoryCacheService.HistoryCountKey, Integer> supplier = k -> myTxService
.withRequest(myRequest)
.withRequestPartitionId(getRequestPartitionId())
.execute(() -> {
HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder(mySearchEntity.getResourceType(), mySearchEntity.getResourceId(), mySearchEntity.getLastUpdatedLow(), mySearchEntity.getLastUpdatedHigh());
Long count = historyBuilder.fetchCount(getRequestPartitionIdForHistory());
Long count = historyBuilder.fetchCount(getRequestPartitionId());
return count.intValue();
});
@ -307,7 +334,10 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
switch (mySearchEntity.getSearchType()) {
case HISTORY:
return myTxService.withRequest(myRequest).execute(() -> doHistoryInTransaction(mySearchEntity.getOffset(), theFromIndex, theToIndex));
return myTxService
.withRequest(myRequest)
.withRequestPartitionId(getRequestPartitionId())
.execute(() -> doHistoryInTransaction(mySearchEntity.getOffset(), theFromIndex, theToIndex));
case SEARCH:
case EVERYTHING:
default:

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.search;
* #L%
*/
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.config.JpaConfig;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.entity.Search;
@ -52,16 +53,16 @@ public class PersistedJpaBundleProviderFactory {
return (PersistedJpaBundleProvider) retVal;
}
public PersistedJpaSearchFirstPageBundleProvider newInstanceFirstPage(RequestDetails theRequestDetails, Search theSearch, SearchTask theTask, ISearchBuilder theSearchBuilder) {
return (PersistedJpaSearchFirstPageBundleProvider) myApplicationContext.getBean(JpaConfig.PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER, theRequestDetails, theSearch, theTask, theSearchBuilder);
public PersistedJpaSearchFirstPageBundleProvider newInstanceFirstPage(RequestDetails theRequestDetails, Search theSearch, SearchTask theTask, ISearchBuilder theSearchBuilder, RequestPartitionId theRequestPartitionId) {
return (PersistedJpaSearchFirstPageBundleProvider) myApplicationContext.getBean(JpaConfig.PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER, theRequestDetails, theSearch, theTask, theSearchBuilder, theRequestPartitionId);
}
public IBundleProvider history(RequestDetails theRequest, String theResourceType, Long theResourcePid, Date theRangeStartInclusive, Date theRangeEndInclusive, Integer theOffset) {
return history(theRequest, theResourceType, theResourcePid, theRangeStartInclusive, theRangeEndInclusive, theOffset, null);
public IBundleProvider history(RequestDetails theRequest, String theResourceType, Long theResourcePid, Date theRangeStartInclusive, Date theRangeEndInclusive, Integer theOffset, RequestPartitionId theRequestPartitionId) {
return history(theRequest, theResourceType, theResourcePid, theRangeStartInclusive, theRangeEndInclusive, theOffset, null, theRequestPartitionId);
}
public IBundleProvider history(RequestDetails theRequest, String theResourceType, Long theResourcePid, Date theRangeStartInclusive, Date theRangeEndInclusive, Integer theOffset, HistorySearchStyleEnum searchParameterType) {
public IBundleProvider history(RequestDetails theRequest, String theResourceType, Long theResourcePid, Date theRangeStartInclusive, Date theRangeEndInclusive, Integer theOffset, HistorySearchStyleEnum searchParameterType, RequestPartitionId theRequestPartitionId) {
String resourceName = defaultIfBlank(theResourceType, null);
Search search = new Search();
@ -76,7 +77,10 @@ public class PersistedJpaBundleProviderFactory {
search.setStatus(SearchStatusEnum.FINISHED);
search.setHistorySearchStyle(searchParameterType);
return newInstance(theRequest, search);
PersistedJpaBundleProvider provider = newInstance(theRequest, search);
provider.setRequestPartitionId(theRequestPartitionId);
return provider;
}
}

View File

@ -20,17 +20,17 @@ package ca.uhn.fhir.jpa.search;
* #L%
*/
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchTask;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -48,11 +48,15 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
/**
* Constructor
*/
public PersistedJpaSearchFirstPageBundleProvider(Search theSearch, SearchTask theSearchTask, ISearchBuilder theSearchBuilder, RequestDetails theRequest) {
public PersistedJpaSearchFirstPageBundleProvider(Search theSearch, SearchTask theSearchTask, ISearchBuilder theSearchBuilder, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
super(theRequest, theSearch.getUuid());
assert theSearch.getSearchType() != SearchTypeEnum.HISTORY;
setSearchEntity(theSearch);
mySearchTask = theSearchTask;
mySearchBuilder = theSearchBuilder;
super.setRequestPartitionId(theRequestPartitionId);
}
@Nonnull
@ -67,9 +71,12 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
final List<JpaPid> pids = mySearchTask.getResourcePids(theFromIndex, theToIndex);
ourLog.trace("Done fetching search resource PIDs");
List<IBaseResource> retVal = myTxService.withRequest(myRequest).execute(() -> {
return toResourceList(mySearchBuilder, pids);
});
RequestPartitionId requestPartitionId = getRequestPartitionId();
List<IBaseResource> retVal = myTxService
.withRequest(myRequest)
.withRequestPartitionId(requestPartitionId)
.execute(() -> toResourceList(mySearchBuilder, pids));
long totalCountWanted = theToIndex - theFromIndex;
long totalCountMatch = (int) retVal

View File

@ -69,7 +69,7 @@ import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.domain.AbstractPageRequest;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
@ -108,7 +108,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
private final SearchBuilderFactory<JpaPid> mySearchBuilderFactory;
private final ISynchronousSearchSvc mySynchronousSearchSvc;
private final PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory;
private final IRequestPartitionHelperSvc myRequestPartitionHelperService;
private final ISearchParamRegistry mySearchParamRegistry;
private final SearchStrategyFactory mySearchStrategyFactory;
private final ExceptionService myExceptionSvc;
@ -154,7 +153,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
mySearchBuilderFactory = theSearchBuilderFactory;
mySynchronousSearchSvc = theSynchronousSearchSvc;
myPersistedJpaBundleProviderFactory = thePersistedJpaBundleProviderFactory;
myRequestPartitionHelperService = theRequestPartitionHelperService;
mySearchParamRegistry = theSearchParamRegistry;
mySearchStrategyFactory = theSearchStrategyFactory;
myExceptionSvc = theExceptionSvc;
@ -201,9 +199,19 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
/**
* This method is called by the HTTP client processing thread in order to
* fetch resources.
* <p>
* This method must not be called from inside a transaction. The rationale is that
* the {@link Search} entity is treated as a piece of shared state across client threads
* accessing the same thread, so we need to be able to update that table in a transaction
* and commit it right away in order for that to work. Examples of needing to do this
* include if two different clients request the same search and are both paging at the
* same time, but also includes clients that are hacking the paging links to
* fetch multiple pages of a search result in parallel. In both cases we need to only
* let one of them actually activate the search, or we will have conficts. The other thread
* just needs to wait until the first one actually fetches more results.
*/
@Override
public List<JpaPid> getResources(final String theUuid, int theFrom, int theTo, @Nullable RequestDetails theRequestDetails) {
public List<JpaPid> getResources(final String theUuid, int theFrom, int theTo, @Nullable RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
assert !TransactionSynchronizationManager.isActualTransactionActive();
// If we're actively searching right now, don't try to do anything until at least one batch has been
@ -240,13 +248,12 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
Callable<Search> searchCallback = () -> mySearchCacheSvc
.fetchByUuid(theUuid)
.orElseThrow(() -> myExceptionSvc.newUnknownSearchException(theUuid));
if (theRequestDetails != null) {
search = myTxService.withRequest(theRequestDetails).execute(searchCallback);
} else {
search = HapiTransactionService.invokeCallableAndHandleAnyException(searchCallback);
}
search = myTxService
.withRequest(theRequestDetails)
.withRequestPartitionId(theRequestPartitionId)
.execute(searchCallback);
QueryParameterUtils.verifySearchHasntFailedOrThrowInternalErrorException(search);
if (search.getStatus() == SearchStatusEnum.FINISHED) {
ourLog.trace("Search entity marked as finished with {} results", search.getNumFound());
break;
@ -272,7 +279,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
String resourceType = search.getResourceType();
SearchParameterMap params = search.getSearchParameterMap().orElseThrow(() -> new IllegalStateException("No map in PASSCOMPLET search"));
IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao(resourceType);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequestDetails, resourceType, params, null);
SearchTaskParameters parameters = new SearchTaskParameters(
search,
@ -280,7 +286,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
params,
resourceType,
theRequestDetails,
requestPartitionId,
theRequestPartitionId,
myOnRemoveSearchTask,
mySyncSize
);
@ -299,7 +305,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
ourLog.trace("Finished looping");
List<JpaPid> pids = fetchResultPids(theUuid, theFrom, theTo, theRequestDetails, search);
List<JpaPid> pids = fetchResultPids(theUuid, theFrom, theTo, theRequestDetails, search, theRequestPartitionId);
ourLog.trace("Fetched {} results", pids.size());
@ -307,8 +313,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
}
@Nonnull
private List<JpaPid> fetchResultPids(String theUuid, int theFrom, int theTo, @Nullable RequestDetails theRequestDetails, Search theSearch) {
List<JpaPid> pids = myTxService.withRequest(theRequestDetails).execute(() -> mySearchResultCacheSvc.fetchResultPids(theSearch, theFrom, theTo));
private List<JpaPid> fetchResultPids(String theUuid, int theFrom, int theTo, @Nullable RequestDetails theRequestDetails, Search theSearch, RequestPartitionId theRequestPartitionId) {
List<JpaPid> pids = mySearchResultCacheSvc.fetchResultPids(theSearch, theFrom, theTo, theRequestDetails, theRequestPartitionId);
if (pids == null) {
throw myExceptionSvc.newUnknownSearchException(theUuid);
}
@ -325,7 +331,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
Search search = new Search();
QueryParameterUtils.populateSearchEntity(theParams, theResourceType, searchUuid, queryString, search, theRequestPartitionId);
myStorageInterceptorHooks.callStoragePresearchRegistered(theRequestDetails, theParams, search);
myStorageInterceptorHooks.callStoragePresearchRegistered(theRequestDetails, theParams, search, theRequestPartitionId);
validateSearch(theParams);
@ -483,7 +489,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
myIdToSearchTask.put(theSearch.getUuid(), task);
task.call();
PersistedJpaSearchFirstPageBundleProvider retVal = myPersistedJpaBundleProviderFactory.newInstanceFirstPage(theRequestDetails, theSearch, task, theSb);
PersistedJpaSearchFirstPageBundleProvider retVal = myPersistedJpaBundleProviderFactory.newInstanceFirstPage(theRequestDetails, theSearch, task, theSb, theRequestPartitionId);
ourLog.debug("Search initial phase completed in {}ms", w.getMillis());
return retVal;
@ -492,7 +498,10 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
@Nullable
private PersistedJpaBundleProvider findCachedQuery(SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails, String theQueryString, RequestPartitionId theRequestPartitionId) {
// May be null
return myTxService.withRequest(theRequestDetails).execute(() -> {
return myTxService
.withRequest(theRequestDetails)
.withRequestPartitionId(theRequestPartitionId)
.execute(() -> {
// Interceptor call: STORAGE_PRECHECK_FOR_CACHED_SEARCH
HookParams params = new HookParams()
@ -563,7 +572,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
int pageIndex = theFromIndex / pageSize;
Pageable page = new AbstractPageRequest(pageIndex, pageSize) {
return new PageRequest(pageIndex, pageSize, Sort.unsorted()) {
private static final long serialVersionUID = 1L;
@Override
@ -571,33 +580,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
return theFromIndex;
}
@Override
public Sort getSort() {
return Sort.unsorted();
}
@Override
public Pageable next() {
return null;
}
@Override
public Pageable previous() {
return null;
}
@Override
public Pageable first() {
return null;
}
@Override
public Pageable withPage(int theI) {
return null;
}
};
return page;
}
}

View File

@ -36,6 +36,7 @@ import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
@ -85,6 +86,9 @@ public class SynchronousSearchSvcImpl implements ISynchronousSearchSvc {
@Autowired
private EntityManager myEntityManager;
@Autowired
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
private int mySyncSize = 250;
@Override
@ -97,8 +101,11 @@ public class SynchronousSearchSvcImpl implements ISynchronousSearchSvc {
boolean wantCount = theParamWantOnlyCount || theParamOrConfigWantCount;
// Execute the query and make sure we return distinct results
return myTxService.withRequest(theRequestDetails).readOnly().execute(() -> {
return myTxService
.withRequest(theRequestDetails)
.withRequestPartitionId(theRequestPartitionId)
.readOnly()
.execute(() -> {
// Load the results synchronously
final List<JpaPid> pids = new ArrayList<>();

View File

@ -878,7 +878,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
resourceId.setVersion(version);
if (version != null && !version.equals(next.getVersion())) {
IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(resourceType);
next = dao.readEntity(next.getIdDt().withVersion(Long.toString(version)), null);
next = (IBaseResourceEntity) dao.readEntity(next.getIdDt().withVersion(Long.toString(version)), null);
}
}

View File

@ -23,8 +23,8 @@ package ca.uhn.fhir.jpa.search.builder;
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.entity.Search;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
@ -46,13 +46,15 @@ public class StorageInterceptorHooksFacade {
* @param theRequestDetails
* @param theParams
* @param search
* @param theRequestPartitionId
*/
public void callStoragePresearchRegistered(RequestDetails theRequestDetails, SearchParameterMap theParams, Search search) {
public void callStoragePresearchRegistered(RequestDetails theRequestDetails, SearchParameterMap theParams, Search search, RequestPartitionId theRequestPartitionId) {
HookParams params = new HookParams()
.add(ICachedSearchDetails.class, search)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(SearchParameterMap.class, theParams);
.add(SearchParameterMap.class, theParams)
.add(RequestPartitionId.class, theRequestPartitionId);
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
}
//private IInterceptorBroadcaster myInterceptorBroadcaster;

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.search.builder.tasks;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
@ -74,8 +75,12 @@ public class SearchContinuationTask extends SearchTask {
@Override
public Void call() {
try {
myTxService.withRequest(myRequestDetails).execute(() -> {
List<JpaPid> previouslyAddedResourcePids = mySearchResultCacheSvc.fetchAllResultPids(getSearch());
RequestPartitionId requestPartitionId = getRequestPartitionId();
myTxService
.withRequest(myRequestDetails)
.withRequestPartitionId(requestPartitionId)
.execute(() -> {
List<JpaPid> previouslyAddedResourcePids = mySearchResultCacheSvc.fetchAllResultPids(getSearch(), myRequestDetails, requestPartitionId);
if (previouslyAddedResourcePids == null) {
throw myExceptionSvc.newUnknownSearchException(getSearch().getUuid());
}

View File

@ -92,7 +92,10 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
public class SearchTask implements Callable<Void> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchTask.class);
// injected beans
protected final HapiTransactionService myTxService;
protected final FhirContext myContext;
protected final ISearchResultCacheSvc mySearchResultCacheSvc;
private final SearchParameterMap myParams;
private final IDao myCallingDao;
private final String myResourceType;
@ -104,6 +107,14 @@ public class SearchTask implements Callable<Void> {
private final RequestPartitionId myRequestPartitionId;
private final SearchRuntimeDetails mySearchRuntimeDetails;
private final Transaction myParentTransaction;
private final Consumer<String> myOnRemove;
private final int mySyncSize;
private final Integer myLoadingThrottleForUnitTests;
private final IInterceptorBroadcaster myInterceptorBroadcaster;
private final SearchBuilderFactory<JpaPid> mySearchBuilderFactory;
private final JpaStorageSettings myStorageSettings;
private final ISearchCacheSvc mySearchCacheSvc;
private final IPagingProvider myPagingProvider;
private Search mySearch;
private boolean myAbortRequested;
private int myCountSavedTotal = 0;
@ -112,22 +123,6 @@ public class SearchTask implements Callable<Void> {
private boolean myAdditionalPrefetchThresholdsRemaining;
private List<JpaPid> myPreviouslyAddedResourcePids;
private Integer myMaxResultsToFetch;
private final Consumer<String> myOnRemove;
private final int mySyncSize;
private final Integer myLoadingThrottleForUnitTests;
// injected beans
protected final HapiTransactionService myTxService;
protected final FhirContext myContext;
private final IInterceptorBroadcaster myInterceptorBroadcaster;
private final SearchBuilderFactory<JpaPid> mySearchBuilderFactory;
protected final ISearchResultCacheSvc mySearchResultCacheSvc;
private final JpaStorageSettings myStorageSettings;
private final ISearchCacheSvc mySearchCacheSvc;
private final IPagingProvider myPagingProvider;
/**
* Constructor
*/
@ -169,6 +164,10 @@ public class SearchTask implements Callable<Void> {
myParentTransaction = ElasticApm.currentTransaction();
}
protected RequestPartitionId getRequestPartitionId() {
return myRequestPartitionId;
}
/**
* This method is called by the server HTTP thread, and
* will block until at least one page of results have been
@ -267,11 +266,18 @@ public class SearchTask implements Callable<Void> {
}
public void saveSearch() {
myTxService.execute(myRequest, null, Propagation.REQUIRES_NEW, Isolation.DEFAULT, ()->doSaveSearch());
myTxService
.withRequest(myRequest)
.withRequestPartitionId(myRequestPartitionId)
.withPropagation(Propagation.REQUIRES_NEW)
.execute(() -> doSaveSearch());
}
private void saveUnsynced(final IResultIterator theResultIter) {
myTxService.withRequest(myRequest).execute(()->{
myTxService
.withRequest(myRequest)
.withRequestPartitionId(myRequestPartitionId)
.execute(() -> {
if (mySearch.getId() == null) {
doSaveSearch();
}
@ -303,7 +309,7 @@ public class SearchTask implements Callable<Void> {
// Actually store the results in the query cache storage
myCountSavedTotal += unsyncedPids.size();
myCountSavedThisPass += unsyncedPids.size();
mySearchResultCacheSvc.storeResults(mySearch, mySyncedPids, unsyncedPids);
mySearchResultCacheSvc.storeResults(mySearch, mySyncedPids, unsyncedPids, myRequest, getRequestPartitionId());
synchronized (mySyncedPids) {
int numSyncedThisPass = unsyncedPids.size();
@ -387,7 +393,11 @@ public class SearchTask implements Callable<Void> {
// Create an initial search in the DB and give it an ID
saveSearch();
myTxService.execute(myRequest, null, Propagation.REQUIRED, Isolation.READ_COMMITTED, ()->doSearch());
myTxService
.withRequest(myRequest)
.withRequestPartitionId(myRequestPartitionId)
.withIsolation(Isolation.READ_COMMITTED)
.execute(() -> doSearch());
mySearchRuntimeDetails.setSearchStatus(mySearch.getStatus());
if (mySearch.getStatus() == SearchStatusEnum.FINISHED) {
@ -506,7 +516,10 @@ public class SearchTask implements Callable<Void> {
ourLog.trace("Got count {}", count);
myTxService.withRequest(myRequest).execute(()->{
myTxService
.withRequest(myRequest)
.withRequestPartitionId(myRequestPartitionId)
.execute(() -> {
mySearch.setTotalCount(count.intValue());
if (myParamWantOnlyCount) {
mySearch.setStatus(SearchStatusEnum.FINISHED);

View File

@ -111,7 +111,6 @@ public class DatabaseSearchCacheSvcImpl implements ISearchCacheSvc {
}
@Override
@Transactional(propagation = Propagation.NEVER)
public Optional<Search> tryToMarkSearchAsInProgress(Search theSearch) {
ourLog.trace("Going to try to change search status from {} to {}", theSearch.getStatus(), SearchStatusEnum.LOADING);
try {

View File

@ -20,17 +20,18 @@ package ca.uhn.fhir.jpa.search.cache;
* #L%
*/
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchResult;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.List;
@ -43,9 +44,15 @@ public class DatabaseSearchResultCacheSvcImpl implements ISearchResultCacheSvc {
@Autowired
private ISearchResultDao mySearchResultDao;
@Autowired
private IHapiTransactionService myTransactionService;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public List<JpaPid> fetchResultPids(Search theSearch, int theFrom, int theTo) {
public List<JpaPid> fetchResultPids(Search theSearch, int theFrom, int theTo, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
return myTransactionService
.withRequest(theRequestDetails)
.withRequestPartitionId(theRequestPartitionId)
.execute(() -> {
final Pageable page = toPage(theFrom, theTo);
if (page == null) {
return Collections.emptyList();
@ -58,19 +65,27 @@ public class DatabaseSearchResultCacheSvcImpl implements ISearchResultCacheSvc {
ourLog.debug("fetchResultPids for range {}-{} returned {} pids", theFrom, theTo, retVal.size());
return JpaPid.fromLongList(retVal);
});
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public List<JpaPid> fetchAllResultPids(Search theSearch) {
public List<JpaPid> fetchAllResultPids(Search theSearch, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
return myTransactionService
.withRequest(theRequestDetails)
.withRequestPartitionId(theRequestPartitionId)
.execute(() -> {
List<Long> retVal = mySearchResultDao.findWithSearchPidOrderIndependent(theSearch.getId());
ourLog.trace("fetchAllResultPids returned {} pids", retVal.size());
return JpaPid.fromLongList(retVal);
});
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void storeResults(Search theSearch, List<JpaPid> thePreviouslyStoredResourcePids, List<JpaPid> theNewResourcePids) {
public void storeResults(Search theSearch, List<JpaPid> thePreviouslyStoredResourcePids, List<JpaPid> theNewResourcePids, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
myTransactionService
.withRequest(theRequestDetails)
.withRequestPartitionId(theRequestPartitionId)
.execute(() -> {
List<SearchResult> resultsToSave = Lists.newArrayList();
ourLog.debug("Storing {} results with {} previous for search", theNewResourcePids.size(), thePreviouslyStoredResourcePids.size());
@ -87,6 +102,7 @@ public class DatabaseSearchResultCacheSvcImpl implements ISearchResultCacheSvc {
}
mySearchResultDao.saveAll(resultsToSave);
});
}
}

View File

@ -20,8 +20,10 @@ package ca.uhn.fhir.jpa.search.cache;
* #L%
*/
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nullable;
import java.util.List;
@ -30,12 +32,12 @@ public interface ISearchResultCacheSvc {
/**
* @param theSearch The search - This method is not required to persist any chances to the Search object, it is only provided here for identification
* @param thePreviouslyStoredResourcePids A list of resource PIDs that have previously been saved to this search
* @param theNewResourcePids A list of new resoure PIDs to add to this search (these ones have not been previously saved)
* @param theNewResourcePids A list of new resource PIDs to add to this search (these ones have not been previously saved)
*/
void storeResults(Search theSearch, List<JpaPid> thePreviouslyStoredResourcePids, List<JpaPid> theNewResourcePids);
void storeResults(Search theSearch, List<JpaPid> thePreviouslyStoredResourcePids, List<JpaPid> theNewResourcePids, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId);
/**
* Fetch a sunset of the search result IDs from the cache
* Fetch a subset of the search result IDs from the cache
*
* @param theSearch The search to fetch IDs for
* @param theFrom The starting index (inclusive)
@ -44,7 +46,7 @@ public interface ISearchResultCacheSvc {
* have been removed from the cache for some reason, such as expiry or manual purge)
*/
@Nullable
List<JpaPid> fetchResultPids(Search theSearch, int theFrom, int theTo);
List<JpaPid> fetchResultPids(Search theSearch, int theFrom, int theTo, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId);
/**
* Fetch all result PIDs for a given search with no particular order required
@ -54,6 +56,6 @@ public interface ISearchResultCacheSvc {
* have been removed from the cache for some reason, such as expiry or manual purge)
*/
@Nullable
List<JpaPid> fetchAllResultPids(Search theSearch);
List<JpaPid> fetchAllResultPids(Search theSearch, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId);
}

View File

@ -0,0 +1,45 @@
package ca.uhn.fhir.jpa.util;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import org.springframework.orm.jpa.JpaDialect;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
public class JpaHapiTransactionService extends HapiTransactionService {
private volatile Boolean myCustomIsolationSupported;
@Override
public boolean isCustomIsolationSupported() {
if (myCustomIsolationSupported == null) {
if (myTransactionManager instanceof JpaTransactionManager) {
JpaDialect jpaDialect = ((JpaTransactionManager) myTransactionManager).getJpaDialect();
myCustomIsolationSupported = (jpaDialect instanceof HibernateJpaDialect);
} else {
myCustomIsolationSupported = false;
}
}
return myCustomIsolationSupported;
}
}

View File

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

View File

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

View File

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

View File

@ -44,7 +44,6 @@ import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.provider.MdmControllerHelper;
import ca.uhn.fhir.mdm.provider.MdmControllerUtil;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
@ -125,9 +124,8 @@ public class MdmControllerSvcImpl implements IMdmControllerSvc {
}
@Override
public Page<MdmLinkJson> queryLinks(MdmQuerySearchParameters theMdmQuerySearchParameters, MdmTransactionContext theMdmTransactionContext, RequestDetails theRequestDetails)
{
RequestPartitionId theReadPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, null, null);
public Page<MdmLinkJson> queryLinks(MdmQuerySearchParameters theMdmQuerySearchParameters, MdmTransactionContext theMdmTransactionContext, RequestDetails theRequestDetails) {
RequestPartitionId theReadPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, null);
Page<MdmLinkJson> resultPage;
if (theReadPartitionId.hasPartitionIds()) {
theMdmQuerySearchParameters.setPartitionIds(theReadPartitionId.getPartitionIds());
@ -166,7 +164,7 @@ public class MdmControllerSvcImpl implements IMdmControllerSvc {
@Override
public Page<MdmLinkJson> getDuplicateGoldenResources(MdmTransactionContext theMdmTransactionContext, MdmPageRequest thePageRequest, RequestDetails theRequestDetails, String theRequestResourceType) {
Page<MdmLinkJson> resultPage;
RequestPartitionId readPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, null, null);
RequestPartitionId readPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, null);
if (readPartitionId.isAllPartitions()) {
resultPage = myMdmLinkQuerySvc.getDuplicateGoldenResources(theMdmTransactionContext, thePageRequest, null, theRequestResourceType);
@ -209,8 +207,8 @@ public class MdmControllerSvcImpl implements IMdmControllerSvc {
params.setBatchSize(theBatchSize.getValue().intValue());
}
ReadPartitionIdRequestDetails details= new ReadPartitionIdRequestDetails(null, RestOperationTypeEnum.EXTENDED_OPERATION_SERVER, null, null, null);
RequestPartitionId requestPartition = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, null, details);
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forOperation(null, null, ProviderConstants.OPERATION_MDM_CLEAR);
RequestPartitionId requestPartition = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, details);
params.setRequestPartitionId(requestPartition);
JobInstanceStartRequest request = new JobInstanceStartRequest();

View File

@ -36,7 +36,7 @@ public class MdmProviderMatchR4Test extends BaseProviderR4Test {
}
@Test
public void testMatch() throws Exception {
public void testMatch() {
Patient jane = buildJanePatient();
jane.setActive(true);
Patient createdJane = createPatient(jane);

View File

@ -32,6 +32,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.domain.Page;
@ -41,6 +43,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import static ca.uhn.fhir.mdm.provider.MdmProviderDstu3Plus.DEFAULT_PAGE_SIZE;
import static ca.uhn.fhir.mdm.provider.MdmProviderDstu3Plus.MAX_PAGE_SIZE;
@ -50,6 +53,8 @@ import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
public class MdmControllerSvcImplTest extends BaseLinkR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(MdmControllerSvcImplTest.class);
@Autowired
IMdmControllerSvc myMdmControllerSvc;
@ -137,13 +142,18 @@ public class MdmControllerSvcImplTest extends BaseLinkR4Test {
assertEquals(MdmLinkSourceEnum.AUTO, link.getLinkSource());
assertLinkCount(2);
runInTransaction(()->{
ourLog.info("Links: {}", myMdmLinkDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * ")));
});
myCaptureQueriesListener.clear();
Page<MdmLinkJson> resultPage = myMdmControllerSvc.getDuplicateGoldenResources(null,
new MdmPageRequest((Integer) null, null, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE),
new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.fromPartitionId(1)), null);
myCaptureQueriesListener.logSelectQueries();
assertEquals(resultPage.getContent().size(), 1);
assertEquals(resultPage.getContent().get(0).getSourceId(), patient.getIdElement().getResourceType() + "/" + patient.getIdElement().getIdPart());
assertEquals(1, resultPage.getContent().size());
assertEquals(patient.getIdElement().getResourceType() + "/" + patient.getIdElement().getIdPart(), resultPage.getContent().get(0).getSourceId());
Mockito.verify(myRequestPartitionHelperSvc, Mockito.atLeastOnce()).validateHasPartitionPermissions(any(), eq("Patient"), argThat(new PartitionIdMatcher(requestPartitionId)));
}

View File

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

View File

@ -30,6 +30,25 @@ public class PartitionSettings {
private boolean myIncludePartitionInSearchHashes = false;
private boolean myUnnamedPartitionMode;
private Integer myDefaultPartitionId;
private boolean myAlwaysOpenNewTransactionForDifferentPartition;
/**
* Should we always open a new database transaction if the partition context changes
*
* @since 6.6.0
*/
public boolean isAlwaysOpenNewTransactionForDifferentPartition() {
return myAlwaysOpenNewTransactionForDifferentPartition;
}
/**
* Should we always open a new database transaction if the partition context changes
*
* @since 6.6.0
*/
public void setAlwaysOpenNewTransactionForDifferentPartition(boolean theAlwaysOpenNewTransactionForDifferentPartition) {
myAlwaysOpenNewTransactionForDifferentPartition = theAlwaysOpenNewTransactionForDifferentPartition;
}
/**
* If set to <code>true</code> (default is <code>false</code>) the <code>PARTITION_ID</code> value will be factored into the
@ -136,6 +155,12 @@ public class PartitionSettings {
myDefaultPartitionId = theDefaultPartitionId;
}
/**
* If enabled the JPA server will allow unqualified cross partition reference
*/
public boolean isAllowUnqualifiedCrossPartitionReference() {
return myAllowReferencesAcrossPartitions.equals(PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED);
}
public enum CrossPartitionReferenceMode {
@ -145,18 +170,11 @@ public class PartitionSettings {
NOT_ALLOWED,
/**
* References can cross partition boundaries, in a way that hides the existence of partitions to the end user
* References can cross partition boundaries, with an assumption that boundaries
* will be managed by the database.
*/
ALLOWED_UNQUALIFIED
ALLOWED_UNQUALIFIED,
}
/**
* If enabled the JPA server will allow unqualified cross partition reference
*
*/
public boolean isAllowUnqualifiedCrossPartitionReference() {
return myAllowReferencesAcrossPartitions.equals(PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED);
}
}

View File

@ -20,9 +20,10 @@ package ca.uhn.fhir.jpa.model.cross;
* #L%
*/
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import org.hl7.fhir.instance.model.api.IIdType;
public interface IBasePersistedResource extends IResourceLookup {
public interface IBasePersistedResource<T extends IResourcePersistentId<?>> extends IResourceLookup<T> {
IIdType getIdDt();

View File

@ -24,7 +24,7 @@ import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import java.util.Date;
public interface IResourceLookup<T extends IResourcePersistentId> {
public interface IResourceLookup<T extends IResourcePersistentId<?>> {
String getResourceType();
/**

View File

@ -65,8 +65,12 @@ public class HapiSequenceStyleGenerator implements IdentifierGenerator, Persiste
@Override
public Serializable generate(SharedSessionContractImplementor theSession, Object theObject) throws HibernateException {
Long retVal = myIdMassager.generate(myGeneratorName);
if (retVal == null) {
Long next = (Long) myGen.generate(theSession, theObject);
return myIdMassager.massage(myGeneratorName, next);
retVal = myIdMassager.massage(myGeneratorName, next);
}
return retVal;
}
@Override

View File

@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.model.dialect;
* #L%
*/
import javax.annotation.Nullable;
/**
* This is an internal API and may change or disappear without notice
*
@ -29,6 +31,17 @@ public interface ISequenceValueMassager {
Long massage(String theGeneratorName, Long theId);
/**
* If desired, the massager can supply an ID on its own. This method is tried first,
* and if it returns null, the normal hi-lo generator is called and then
* {@link #massage(String, Long)} is called on the output of that.
*
* @return Returns an ID or null
*/
@Nullable
default Long generate(String theGeneratorName) {
return null;
}
final class NoopSequenceValueMassager implements ISequenceValueMassager {

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.model.entity;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.model.primitive.InstantDt;
import org.hibernate.annotations.OptimisticLock;
@ -38,8 +39,10 @@ import java.util.Date;
import static org.apache.commons.lang3.StringUtils.defaultString;
@MappedSuperclass
public abstract class BaseHasResource extends BasePartitionable implements IBaseResourceEntity, IBasePersistedResource {
public abstract class BaseHasResource extends BasePartitionable implements IBaseResourceEntity, IBasePersistedResource<JpaPid> {
public static final String RES_PUBLISHED = "RES_PUBLISHED";
public static final String RES_UPDATED = "RES_UPDATED";
@Column(name = "RES_DELETED_AT", nullable = true)
@Temporal(TemporalType.TIMESTAMP)
private Date myDeleted;
@ -54,12 +57,12 @@ public abstract class BaseHasResource extends BasePartitionable implements IBase
private boolean myHasTags;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "RES_PUBLISHED", nullable = false)
@Column(name = RES_PUBLISHED, nullable = false)
@OptimisticLock(excluded = true)
private Date myPublished;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "RES_UPDATED", nullable = false)
@Column(name = RES_UPDATED, nullable = false)
@OptimisticLock(excluded = true)
private Date myUpdated;

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.model.entity;
*/
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
@ -118,4 +119,14 @@ public class PartitionablePartitionId implements Cloneable {
return RequestPartitionId.defaultPartition();
}
}
@Nonnull
public static PartitionablePartitionId toStoragePartition(@Nonnull RequestPartitionId theRequestPartitionId, @Nonnull PartitionSettings thePartitionSettings) {
Integer partitionId = theRequestPartitionId.getFirstPartitionIdOrNull();
if (partitionId == null) {
partitionId = thePartitionSettings.getDefaultPartitionId();
}
return new PartitionablePartitionId(partitionId, theRequestPartitionId.getPartitionDate());
}
}

View File

@ -23,9 +23,6 @@ package ca.uhn.fhir.jpa.model.entity;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
/**
* @see ResourceHistoryTable#ENCODING_COL_LENGTH
*/
public enum ResourceEncodingEnum {
/*
@ -47,7 +44,13 @@ public enum ResourceEncodingEnum {
/**
* Resource was deleted - No contents expected
*/
DEL;
DEL,
/**
* Externally stored resource - Resource text is a reference to an external storage location,
* which will be stored in {@link ResourceHistoryTable#getResourceTextVc()}
*/
ESR;
public IParser newParser(FhirContext theContext) {
return theContext.newJsonParser();

View File

@ -92,6 +92,7 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl
@Column(name = "RES_TEXT_VC", length = RES_TEXT_VC_MAX_LENGTH, nullable = true)
@OptimisticLock(excluded = true)
private String myResourceTextVc;
@Column(name = "RES_ENCODING", nullable = false, length = ENCODING_COL_LENGTH)
@Enumerated(EnumType.STRING)
@OptimisticLock(excluded = true)

View File

@ -98,6 +98,9 @@ public class ResourceLink extends BaseResourceIndex {
@Transient
private transient String myTargetResourceId;
/**
* Constructor
*/
public ResourceLink() {
super();
}

View File

@ -78,16 +78,18 @@ import java.util.stream.Collectors;
@Indexed(routingBinder= @RoutingBinderRef(type = ResourceTableRoutingBinder.class))
@Entity
@Table(name = "HFJ_RESOURCE", uniqueConstraints = {}, indexes = {
@Table(name = ResourceTable.HFJ_RESOURCE, uniqueConstraints = {}, indexes = {
// Do not reuse previously used index name: IDX_INDEXSTATUS, IDX_RES_TYPE
@Index(name = "IDX_RES_DATE", columnList = "RES_UPDATED"),
@Index(name = "IDX_RES_DATE", columnList = BaseHasResource.RES_UPDATED),
@Index(name = "IDX_RES_TYPE_DEL_UPDATED", columnList = "RES_TYPE,RES_DELETED_AT,RES_UPDATED,PARTITION_ID,RES_ID"),
})
@NamedEntityGraph(name = "Resource.noJoins")
public class ResourceTable extends BaseHasResource implements Serializable, IBasePersistedResource, IResourceLookup {
public class ResourceTable extends BaseHasResource implements Serializable, IBasePersistedResource<JpaPid> {
public static final int RESTYPE_LEN = 40;
private static final int MAX_LANGUAGE_LENGTH = 20;
private static final long serialVersionUID = 1L;
public static final String HFJ_RESOURCE = "HFJ_RESOURCE";
public static final String RES_TYPE = "RES_TYPE";
/**
* Holds the narrative text only - Used for Fulltext searching but not directly stored in the DB
@ -263,7 +265,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
@OptimisticLock(excluded = true)
private Collection<ResourceLink> myResourceLinksAsTarget;
@Column(name = "RES_TYPE", length = RESTYPE_LEN, nullable = false)
@Column(name = RES_TYPE, length = RESTYPE_LEN, nullable = false)
@FullTextField
@OptimisticLock(excluded = true)
private String myResourceType;
@ -769,7 +771,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
}
@Override
public IResourcePersistentId getPersistentId() {
public JpaPid getPersistentId() {
return JpaPid.fromId(getId());
}

View File

@ -42,6 +42,10 @@ import java.util.Set;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
/**
* This class contains configuration options common to all hapi-fhir-storage implementations.
* Ultimately it should live in that project
*/
public class StorageSettings {
/**
* @since 5.6.0

View File

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

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.interceptor.model;
/*-
* #%L
* HAPI FHIR - Core Library
* HAPI FHIR Search Parameters
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
@ -20,31 +20,52 @@ package ca.uhn.fhir.interceptor.model;
* #L%
*/
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This is a model class used as a parameter for the interceptor pointcut
* {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PARTITION_IDENTIFY_READ}.
*/
public class ReadPartitionIdRequestDetails extends PartitionIdRequestDetails {
private final String myResourceType;
private final RestOperationTypeEnum myRestOperationType;
private final IIdType myReadResourceId;
private final Object mySearchParams;
@Nullable
private final SearchParameterMap mySearchParams;
@Nullable
private final IBaseResource myConditionalTargetOrNull;
@Nullable
private final String mySearchUuid;
@Nullable
private final String myExtendedOperationName;
public ReadPartitionIdRequestDetails(String theResourceType, RestOperationTypeEnum theRestOperationType, IIdType theReadResourceId, Object theSearchParams, @Nullable IBaseResource theConditionalTargetOrNull) {
private ReadPartitionIdRequestDetails(String theResourceType, RestOperationTypeEnum theRestOperationType, IIdType theReadResourceId, @Nullable SearchParameterMap theSearchParams, @Nullable IBaseResource theConditionalTargetOrNull, @Nullable String theSearchUuid, String theExtendedOperationName) {
myResourceType = theResourceType;
myRestOperationType = theRestOperationType;
myReadResourceId = theReadResourceId;
mySearchParams = theSearchParams;
myConditionalTargetOrNull = theConditionalTargetOrNull;
mySearchUuid = theSearchUuid;
myExtendedOperationName = theExtendedOperationName;
}
public static ReadPartitionIdRequestDetails forRead(String theResourceType, IIdType theId, boolean theIsVread) {
RestOperationTypeEnum op = theIsVread ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ;
return new ReadPartitionIdRequestDetails(theResourceType, op, theId.withResourceType(theResourceType), null, null);
@Nullable
public String getExtendedOperationName() {
return myExtendedOperationName;
}
@Nullable
public String getSearchUuid() {
return mySearchUuid;
}
public String getResourceType() {
@ -59,18 +80,45 @@ public class ReadPartitionIdRequestDetails extends PartitionIdRequestDetails {
return myReadResourceId;
}
public Object getSearchParams() {
@Nullable
public SearchParameterMap getSearchParams() {
return mySearchParams;
}
@Nullable
public IBaseResource getConditionalTargetOrNull() {
return myConditionalTargetOrNull;
}
/**
* @param theId The resource ID (must include a resource type and ID)
*/
public static ReadPartitionIdRequestDetails forRead(IIdType theId) {
assert isNotBlank(theId.getResourceType());
assert isNotBlank(theId.getIdPart());
return forRead(theId.getResourceType(), theId, false);
}
public static ReadPartitionIdRequestDetails forOperation(@Nullable String theResourceType, @Nullable IIdType theId, @Nonnull String theExtendedOperationName) {
RestOperationTypeEnum op;
if (theId != null) {
op = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
} else if (theResourceType != null) {
op = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
} else {
op = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
}
public static ReadPartitionIdRequestDetails forSearchType(String theResourceType, Object theParams, IBaseResource theConditionalOperationTargetOrNull) {
return new ReadPartitionIdRequestDetails(theResourceType, RestOperationTypeEnum.SEARCH_TYPE, null, theParams, theConditionalOperationTargetOrNull);
return new ReadPartitionIdRequestDetails(theResourceType, op, null, null, null, null, theExtendedOperationName);
}
public static ReadPartitionIdRequestDetails forRead(String theResourceType, @Nonnull IIdType theId, boolean theIsVread) {
RestOperationTypeEnum op = theIsVread ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ;
return new ReadPartitionIdRequestDetails(theResourceType, op, theId.withResourceType(theResourceType), null, null, null, null);
}
public static ReadPartitionIdRequestDetails forSearchType(String theResourceType, SearchParameterMap theParams, IBaseResource theConditionalOperationTargetOrNull) {
return new ReadPartitionIdRequestDetails(theResourceType, RestOperationTypeEnum.SEARCH_TYPE, null, theParams, theConditionalOperationTargetOrNull, null, null);
}
public static ReadPartitionIdRequestDetails forHistory(String theResourceType, IIdType theIdType) {
@ -82,6 +130,10 @@ public class ReadPartitionIdRequestDetails extends PartitionIdRequestDetails {
} else {
restOperationTypeEnum = RestOperationTypeEnum.HISTORY_SYSTEM;
}
return new ReadPartitionIdRequestDetails(theResourceType, restOperationTypeEnum, theIdType, null, null);
return new ReadPartitionIdRequestDetails(theResourceType, restOperationTypeEnum, theIdType, null, null, null, null);
}
public static ReadPartitionIdRequestDetails forSearchUuid(String theUuid) {
return new ReadPartitionIdRequestDetails(null, RestOperationTypeEnum.GET_PAGE, null, null, null, theUuid, null);
}
}

View File

@ -28,8 +28,8 @@ import javax.annotation.Nonnull;
import java.util.List;
/**
* This interface is used by the {@link IResourceChangeListenerCacheRefresher} to read resources matching the provided
* search parameter map in the repository and compare them to caches stored in the {@link IResourceChangeListenerRegistry}.
* This interface is used by the {@literal IResourceChangeListenerCacheRefresher} to read resources matching the provided
* search parameter map in the repository and compare them to caches stored in the {@literal IResourceChangeListenerRegistry}.
*/
public interface IResourceVersionSvc {
@Nonnull

View File

@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.cache;
* #L%
*/
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.model.primitive.IdDt;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -44,7 +44,7 @@ public class ResourceVersionMap {
private final Map<IIdType, Long> myMap = new HashMap<>();
private ResourceVersionMap() {}
public static ResourceVersionMap fromResourceTableEntities(List<ResourceTable> theEntities) {
public static ResourceVersionMap fromResourceTableEntities(List<? extends IBasePersistedResource> theEntities) {
ResourceVersionMap retval = new ResourceVersionMap();
theEntities.forEach(entity -> retval.add(entity.getIdDt()));
return retval;

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.partition;
/*-
* #%L
* HAPI FHIR Storage api
* HAPI FHIR Search Parameters
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.partition;
import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -35,18 +34,18 @@ import java.util.Set;
public interface IRequestPartitionHelperSvc {
@Nonnull
RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType, ReadPartitionIdRequestDetails theDetails);
RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, ReadPartitionIdRequestDetails theDetails);
@Nonnull
default RequestPartitionId determineReadPartitionForRequestForRead(RequestDetails theRequest, String theResourceType, IIdType theId) {
default RequestPartitionId determineReadPartitionForRequestForRead(RequestDetails theRequest, String theResourceType, @Nonnull IIdType theId) {
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forRead(theResourceType, theId, theId.hasVersionIdPart());
return determineReadPartitionForRequest(theRequest, theResourceType, details);
return determineReadPartitionForRequest(theRequest, details);
}
@Nonnull
default RequestPartitionId determineReadPartitionForRequestForSearchType(RequestDetails theRequest, String theResourceType, SearchParameterMap theParams, IBaseResource theConditionalOperationTargetOrNull) {
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forSearchType(theResourceType, theParams, theConditionalOperationTargetOrNull);
return determineReadPartitionForRequest(theRequest, theResourceType, details);
return determineReadPartitionForRequest(theRequest, details);
}
RequestPartitionId determineGenericPartitionForRequest(RequestDetails theRequestDetails);
@ -54,18 +53,16 @@ public interface IRequestPartitionHelperSvc {
@Nonnull
default RequestPartitionId determineReadPartitionForRequestForHistory(RequestDetails theRequest, String theResourceType, IIdType theIdType) {
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(theResourceType, theIdType);
return determineReadPartitionForRequest(theRequest, theResourceType, details);
return determineReadPartitionForRequest(theRequest, details);
}
@Nonnull
default void validateHasPartitionPermissions(RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId){}
default void validateHasPartitionPermissions(RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) {
}
@Nonnull
RequestPartitionId determineCreatePartitionForRequest(@Nullable RequestDetails theRequest, @Nonnull IBaseResource theResource, @Nonnull String theResourceType);
@Nonnull
PartitionablePartitionId toStoragePartition(@Nonnull RequestPartitionId theRequestPartitionId);
@Nonnull
Set<Integer> toReadPartitions(@Nonnull RequestPartitionId theRequestPartitionId);

View File

@ -121,8 +121,8 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
private BaseRuntimeChildDefinition myRangeHighValueChild;
private BaseRuntimeChildDefinition myAddressLineValueChild;
private BaseRuntimeChildDefinition myAddressCityValueChild;
private BaseRuntimeChildDefinition myAddressStateValueChild;
private BaseRuntimeChildDefinition myAddressDistrictValueChild;
private BaseRuntimeChildDefinition myAddressStateValueChild;
private BaseRuntimeChildDefinition myAddressCountryValueChild;
private BaseRuntimeChildDefinition myAddressPostalCodeValueChild;
private BaseRuntimeChildDefinition myCapabilityStatementRestSecurityServiceValueChild;

View File

@ -0,0 +1,78 @@
package ca.uhn.fhir.jpa.searchparam.extractor;
/*-
* #%L
* HAPI FHIR Search Parameters
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import javax.annotation.Nonnull;
public class CrossPartitionReferenceDetails {
@Nonnull
private final RequestPartitionId mySourceResourcePartitionId;
@Nonnull
private final PathAndRef myPathAndRef;
@Nonnull
private final RequestDetails myRequestDetails;
@Nonnull
private final TransactionDetails myTransactionDetails;
@Nonnull
private final String mySourceResourceName;
/**
* Constructor
*/
public CrossPartitionReferenceDetails(@Nonnull RequestPartitionId theSourceResourcePartitionId, @Nonnull String theSourceResourceName, @Nonnull PathAndRef thePathAndRef, @Nonnull RequestDetails theRequestDetails, @Nonnull TransactionDetails theTransactionDetails) {
mySourceResourcePartitionId = theSourceResourcePartitionId;
mySourceResourceName = theSourceResourceName;
myPathAndRef = thePathAndRef;
myRequestDetails = theRequestDetails;
myTransactionDetails = theTransactionDetails;
}
@Nonnull
public String getSourceResourceName() {
return mySourceResourceName;
}
@Nonnull
public RequestDetails getRequestDetails() {
return myRequestDetails;
}
@Nonnull
public TransactionDetails getTransactionDetails() {
return myTransactionDetails;
}
@Nonnull
public RequestPartitionId getSourceResourcePartitionId() {
return mySourceResourcePartitionId;
}
@Nonnull
public PathAndRef getPathAndRef() {
return myPathAndRef;
}
}

View File

@ -31,8 +31,8 @@ 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.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.jpa.model.entity.BasePartitionable;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.BasePartitionable;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.IResourceIndexComboSearchParameter;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
@ -50,6 +50,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
@ -71,8 +72,8 @@ import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Optional;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -93,6 +94,8 @@ public class SearchParamExtractorService {
private PartitionSettings myPartitionSettings;
@Autowired(required = false)
private IResourceLinkResolver myResourceLinkResolver;
@Autowired
private IRequestPartitionHelperSvc myPartitionHelperSvc;
@VisibleForTesting
public void setSearchParamExtractor(ISearchParamExtractor theSearchParamExtractor) {
@ -567,19 +570,33 @@ public class SearchParamExtractorService {
* target any more times than we have to.
*/
RequestPartitionId targetRequestPartitionId = theRequestPartitionId;
if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.getAllowReferencesAcrossPartitions() == PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED) {
targetRequestPartitionId = RequestPartitionId.allPartitions();
IResourceLookup<JpaPid> targetResource;
if (myPartitionSettings.isPartitioningEnabled()) {
if (myPartitionSettings.getAllowReferencesAcrossPartitions() == PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED) {
// Interceptor: Pointcut.JPA_CROSS_PARTITION_REFERENCE_DETECTED
if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE, myInterceptorBroadcaster, theRequest)) {
CrossPartitionReferenceDetails referenceDetails = new CrossPartitionReferenceDetails(theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
HookParams params = new HookParams(referenceDetails);
targetResource = (IResourceLookup<JpaPid>) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE, params);
} else {
targetResource = myResourceLinkResolver.findTargetResource(RequestPartitionId.allPartitions(), theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
}
} else {
targetResource = myResourceLinkResolver.findTargetResource(theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
}
} else {
targetResource = myResourceLinkResolver.findTargetResource(theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
}
IResourceLookup targetResource = myResourceLinkResolver.findTargetResource(targetRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
if (targetResource == null) {
return null;
}
String targetResourceType = targetResource.getResourceType();
Long targetResourcePid = ((JpaPid) targetResource.getPersistentId()).getId();
Long targetResourcePid = targetResource.getPersistentId().getId();
String targetResourceIdPart = theNextId.getIdPart();
Long targetVersion = theNextId.getVersionIdPartAsLong();
return ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime, targetVersion);
@ -671,6 +688,7 @@ public class SearchParamExtractorService {
}
// If extraction generated any warnings, broadcast an error
if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING, theInterceptorBroadcaster, theRequestDetails)) {
for (String next : theSearchParamSet.getWarnings()) {
StorageProcessingMessage messageHolder = new StorageProcessingMessage();
messageHolder.setMessage(next);
@ -682,4 +700,5 @@ public class SearchParamExtractorService {
}
}
}
}

View File

@ -36,9 +36,11 @@ public class SearchParamExtractorServiceTest {
searchParamSet.addWarning("help i'm a bug");
searchParamSet.addWarning("Spiff");
when(myJpaInterceptorBroadcaster.hasHooks(any())).thenReturn(true);
when(myJpaInterceptorBroadcaster.callHooks(any(), any())).thenReturn(true);
SearchParamExtractorService.handleWarnings(new ServletRequestDetails(myRequestInterceptorBroadcaster), myJpaInterceptorBroadcaster, searchParamSet);
ServletRequestDetails requestDetails = new ServletRequestDetails(myRequestInterceptorBroadcaster);
SearchParamExtractorService.handleWarnings(requestDetails, myJpaInterceptorBroadcaster, searchParamSet);
verify(myJpaInterceptorBroadcaster, times(2)).callHooks(eq(Pointcut.JPA_PERFTRACE_WARNING), any());
verify(myRequestInterceptorBroadcaster, times(2)).callHooks(eq(Pointcut.JPA_PERFTRACE_WARNING), any());

View File

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

View File

@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.api.svc.ISearchSvc;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.model.sched.HapiJob;
import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
@ -103,6 +104,8 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
private MatchUrlService myMatchUrlService;
@Autowired
private IResourceModifiedConsumer myResourceModifiedConsumer;
@Autowired
private HapiTransactionService myTransactionService;
private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT;
private ExecutorService myExecutorService;
@ -150,8 +153,8 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
SubscriptionTriggeringJobDetails jobDetails = new SubscriptionTriggeringJobDetails();
jobDetails.setJobId(UUID.randomUUID().toString());
jobDetails.setRemainingResourceIds(resourceIds.stream().map(t -> t.getValue()).collect(Collectors.toList()));
jobDetails.setRemainingSearchUrls(searchUrls.stream().map(t -> t.getValue()).collect(Collectors.toList()));
jobDetails.setRemainingResourceIds(resourceIds.stream().map(IPrimitiveType::getValue).collect(Collectors.toList()));
jobDetails.setRemainingSearchUrls(searchUrls.stream().map(IPrimitiveType::getValue).collect(Collectors.toList()));
if (theSubscriptionId != null) {
jobDetails.setSubscriptionId(theSubscriptionId.getIdPart());
}
@ -329,12 +332,17 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao(theJobDetails.getCurrentSearchResourceType());
int maxQuerySize = myMaxSubmitPerPass - totalSubmitted;
int toIndex = fromIndex + maxQuerySize;
int toIndex;
if (theJobDetails.getCurrentSearchCount() != null) {
toIndex = Math.min(toIndex, theJobDetails.getCurrentSearchCount());
toIndex = Math.min(fromIndex + maxQuerySize, theJobDetails.getCurrentSearchCount());
} else {
toIndex = fromIndex + maxQuerySize;
}
ourLog.info("Triggering job[{}] search {} requesting resources {} - {}", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex);
List<IResourcePersistentId> resourceIds = mySearchCoordinatorSvc.getResources(theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex, null);
List<IResourcePersistentId<?>> resourceIds;
RequestPartitionId requestPartitionId = RequestPartitionId.allPartitions();
resourceIds = mySearchCoordinatorSvc.getResources(theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex, null, requestPartitionId);
ourLog.info("Triggering job[{}] delivering {} resources", theJobDetails.getJobId(), resourceIds.size());
int highestIndexSubmitted = theJobDetails.getCurrentSearchLastUploadedIndex();

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.searchparam.config.SearchParamConfig;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory;
@ -76,6 +77,11 @@ public class DaoSubscriptionMatcherTest {
return mock(IResourceVersionSvc.class, RETURNS_DEEP_STUBS);
}
@Bean
public IRequestPartitionHelperSvc requestPartitionHelperSvc() {
return mock(IRequestPartitionHelperSvc.class);
}
}
}

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.subscription.module;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.searchparam.config.SearchParamConfig;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
@ -25,6 +26,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Collections;
import static org.mockito.Mockito.mock;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
SearchParamConfig.class,
@ -84,6 +87,11 @@ public abstract class BaseSubscriptionTest {
return new InterceptorService();
}
@Bean
public IRequestPartitionHelperSvc requestPartitionHelperSvc() {
return mock(IRequestPartitionHelperSvc.class);
}
@Bean
// Default implementation returns the name unchanged
public IChannelNamer channelNamer() {

View File

@ -4,6 +4,7 @@ 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.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionCriteriaParser;
import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionMatchingSubscriber;
@ -20,14 +21,13 @@ import com.google.common.collect.Lists;
import org.hl7.fhir.dstu3.model.BooleanType;
import org.hl7.fhir.dstu3.model.Observation;
import org.hl7.fhir.dstu3.model.Subscription;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Collections;
@ -46,7 +46,6 @@ import static org.mockito.Mockito.when;
* Tests copied from jpa.subscription.resthook.RestHookTestDstu3Test
*/
public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscribableChannelDstu3Test {
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionMatchingSubscriberTest.class);
private final IFhirResourceDao<Subscription> myMockSubscriptionDao = Mockito.mock(IFhirResourceDao.class);
@BeforeEach
@ -55,6 +54,11 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
myDaoRegistry.register(myMockSubscriptionDao);
}
@AfterEach
public void afterEach() {
myStorageSettings.setCrossPartitionSubscriptionEnabled(new JpaStorageSettings().isCrossPartitionSubscriptionEnabled());
}
@Test
public void testRestHookSubscriptionApplicationFhirJson() throws Exception {
String payload = "application/fhir+json";
@ -392,6 +396,13 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
ourObservationListener.awaitExpected();
}
private void mockSubscriptionRead(RequestPartitionId theRequestPartitionId, Subscription subscription) {
Subscription modifiedSubscription = subscription.copy();
// the original partition info was the request info, but we need the actual storage partition.
modifiedSubscription.setUserData(Constants.RESOURCE_PARTITION_ID, theRequestPartitionId);
when(myMockSubscriptionDao.read(eq(subscription.getIdElement()), any())).thenReturn(modifiedSubscription);
}
@Nested
public class TestDeleteMessages {
private final SubscriptionMatchingSubscriber subscriber = new SubscriptionMatchingSubscriber();
@ -496,12 +507,4 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
verify(message, atLeastOnce()).getPayloadId(null);
}
}
private void mockSubscriptionRead(RequestPartitionId theRequestPartitionId, Subscription subscription) {
Subscription modifiedSubscription = subscription.copy();
// the original partition info was the request info, but we need the actual storage partition.
modifiedSubscription.setUserData(Constants.RESOURCE_PARTITION_ID, theRequestPartitionId);
when(myMockSubscriptionDao.read(eq(subscription.getIdElement()), any())).thenReturn(modifiedSubscription);
}
}

View File

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

View File

@ -34,8 +34,10 @@ import static org.junit.jupiter.api.Assertions.fail;
public class FhirResourceDaoDstu2ValidateTest extends BaseJpaDstu2Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu2ValidateTest.class);
@Override
@BeforeEach
public void before() {
public void before() throws Exception {
super.before();
myStorageSettings.setAllowExternalReferences(true);
}

View File

@ -2792,7 +2792,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
* Upload structurredef
*/
String contents = IOUtils.toString(getClass().getResourceAsStream("/allergyintolerance-sd-david.json"), "UTF-8");
String contents = ClasspathUtil.loadResource("/allergyintolerance-sd-david.json");
HttpEntityEnclosingRequestBase post = new HttpPut(myServerBase + "/StructureDefinition/ohAllergyIntolerance");
post.setEntity(new StringEntity(contents, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post);
@ -2809,7 +2809,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
* Validate
*/
contents = IOUtils.toString(getClass().getResourceAsStream("/allergyintolerance-david.json"), "UTF-8");
contents = ClasspathUtil.loadResource("/allergyintolerance-david.json");
post = new HttpPost(myServerBase + "/AllergyIntolerance/$validate?_pretty=true");
post.setEntity(new StringEntity(contents, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8")));

View File

@ -82,9 +82,10 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
private static final Logger ourLog = LoggerFactory.getLogger(SearchCoordinatorSvcImplTest.class);
@Mock private SearchStrategyFactory mySearchStrategyFactory;
@Spy
protected FhirContext myContext = FhirContext.forDstu2Cached();
@Mock
private SearchStrategyFactory mySearchStrategyFactory;
@Mock
private ISearchCacheSvc mySearchCacheSvc;
@Mock
@ -100,9 +101,6 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
private IRequestPartitionHelperSvc myPartitionHelperSvc;
@Mock
private ISynchronousSearchSvc mySynchronousSearchSvc;
@Spy
protected FhirContext myContext = FhirContext.forDstu2Cached();
@Spy
private ExceptionService myExceptionSvc = new ExceptionService(myContext);
@ -118,6 +116,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
@BeforeEach
public void before() {
HapiSystemProperties.enableUnitTestCaptureStack();
HapiSystemProperties.enableUnitTestMode();
myCurrentSearch = null;
@ -178,7 +177,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
assertEquals(allResults.size(), oldResults.size());
allResults.addAll(newResults);
return null;
}).when(mySearchResultCacheSvc).storeResults(any(), anyList(), anyList());
}).when(mySearchResultCacheSvc).storeResults(any(), anyList(), anyList(), any(), any());
SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME"));
@ -233,7 +232,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
});
try {
mySvc.getResources("1234-5678", 0, 100, null);
mySvc.getResources("1234-5678", 0, 100, null, null);
fail();
} catch (ResourceGoneException e) {
assertEquals("Search ID \"1234-5678\" does not exist and may have expired", e.getMessage());
@ -255,7 +254,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
});
try {
mySvc.getResources("1234-5678", 0, 100, null);
mySvc.getResources("1234-5678", 0, 100, null, null);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), containsString("Request timed out"));
@ -295,15 +294,16 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
}
private void initAsyncSearches() {
when(myPersistedJpaBundleProviderFactory.newInstanceFirstPage(nullable(RequestDetails.class), nullable(Search.class), nullable(SearchTask.class), nullable(ISearchBuilder.class))).thenAnswer(t->{
when(myPersistedJpaBundleProviderFactory.newInstanceFirstPage(nullable(RequestDetails.class), nullable(Search.class), nullable(SearchTask.class), nullable(ISearchBuilder.class), nullable(RequestPartitionId.class))).thenAnswer(t -> {
RequestDetails requestDetails = t.getArgument(0, RequestDetails.class);
Search search = t.getArgument(1, Search.class);
SearchTask searchTask = t.getArgument(2, SearchTask.class);
ISearchBuilder<JpaPid> searchBuilder = t.getArgument(3, ISearchBuilder.class);
PersistedJpaSearchFirstPageBundleProvider retVal = new PersistedJpaSearchFirstPageBundleProvider(search, searchTask, searchBuilder, requestDetails);
PersistedJpaSearchFirstPageBundleProvider retVal = new PersistedJpaSearchFirstPageBundleProvider(search, searchTask, searchBuilder, requestDetails, null);
retVal.setStorageSettingsForUnitTest(new JpaStorageSettings());
retVal.setTxServiceForUnitTest(myTransactionService);
retVal.setSearchCoordinatorSvcForUnitTest(mySvc);
retVal.setRequestPartitionHelperSvcForUnitTest(myPartitionHelperSvc);
retVal.setContext(myContext);
return retVal;
});
@ -333,7 +333,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
try {
assertNotNull(searchId);
ourLog.info("About to pull the first resource");
List<JpaPid> resources = mySvc.getResources(searchId, 0, 1, null);
List<JpaPid> resources = mySvc.getResources(searchId, 0, 1, null, null);
ourLog.info("Done pulling the first resource");
assertEquals(1, resources.size());
} finally {
@ -349,7 +349,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
ourLog.info("Done cancelling all searches");
try {
mySvc.getResources(searchId, 0, 1, null);
mySvc.getResources(searchId, 0, 1, null, null);
} catch (ResourceGoneException e) {
// good
}
@ -448,6 +448,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
search.setSearchType(SearchTypeEnum.SEARCH);
search.setResourceType("Patient");
search.setStatus(SearchStatusEnum.LOADING);
search.setSearchParameterMap(new SearchParameterMap());
when(mySearchCacheSvc.fetchByUuid(eq(uuid))).thenReturn(Optional.of(search));
doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
@ -462,7 +463,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
// ignore
}
when(mySearchResultCacheSvc.fetchResultPids(any(Search.class), anyInt(), anyInt())).thenAnswer(theInvocation -> {
when(mySearchResultCacheSvc.fetchResultPids(any(Search.class), anyInt(), anyInt(), any(), any())).thenAnswer(theInvocation -> {
ArrayList<IResourcePersistentId> results = new ArrayList<>();
for (long i = theInvocation.getArgument(1, Integer.class); i < theInvocation.getArgument(2, Integer.class); i++) {
Long nextPid = i + 10L;
@ -492,6 +493,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
provider.setSearchBuilderFactoryForUnitTest(mySearchBuilderFactory);
provider.setSearchCoordinatorSvcForUnitTest(mySvc);
provider.setStorageSettingsForUnitTest(new JpaStorageSettings());
provider.setRequestPartitionId(RequestPartitionId.defaultPartition());
resources = provider.getResources(20, 40);
assertEquals(20, resources.size());
assertEquals("30", resources.get(0).getIdElement().getValueAsString());
@ -511,6 +513,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
provider.setDaoRegistryForUnitTest(myDaoRegistry);
provider.setSearchCoordinatorSvcForUnitTest(mySvc);
provider.setStorageSettingsForUnitTest(new JpaStorageSettings());
provider.setRequestPartitionId(RequestPartitionId.defaultPartition());
return provider;
}
@ -566,10 +569,10 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
search.setTotalCount(100);
when(mySearchCacheSvc.fetchByUuid(eq("0000-1111"))).thenReturn(Optional.of(search));
when(mySearchResultCacheSvc.fetchResultPids(any(), anyInt(), anyInt())).thenReturn(null);
when(mySearchResultCacheSvc.fetchResultPids(any(), anyInt(), anyInt(), any(), any())).thenReturn(null);
try {
mySvc.getResources("0000-1111", 0, 10, null);
mySvc.getResources("0000-1111", 0, 10, null, null);
fail();
} catch (ResourceGoneException e) {
assertEquals("Search ID \"0000-1111\" does not exist and may have expired", e.getMessage());
@ -599,10 +602,10 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
});
mockSearchTask();
when(mySearchResultCacheSvc.fetchAllResultPids(any())).thenReturn(null);
when(mySearchResultCacheSvc.fetchAllResultPids(any(), any(), any())).thenReturn(null);
try {
mySvc.getResources("0000-1111", 0, 10, null);
mySvc.getResources("0000-1111", 0, 10, null, null);
fail();
} catch (ResourceGoneException e) {
assertEquals("Search ID \"0000-1111\" does not exist and may have expired", e.getMessage());
@ -610,10 +613,53 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
}
private void mockSearchTask() {
IPagingProvider pagingProvider = mock(IPagingProvider.class);
lenient().when(pagingProvider.getMaximumPageSize())
.thenReturn(500);
when(myBeanFactory.getBean(anyString(), any(SearchTaskParameters.class)))
.thenAnswer(invocation -> {
String type = invocation.getArgument(0);
switch (type) {
case SearchConfig.SEARCH_TASK -> {
return new SearchTask(
invocation.getArgument(1),
myTransactionService,
ourCtx,
myInterceptorBroadcaster,
mySearchBuilderFactory,
mySearchResultCacheSvc,
myStorageSettings,
mySearchCacheSvc,
pagingProvider
);
}
case SearchConfig.CONTINUE_TASK -> {
return new SearchContinuationTask(
invocation.getArgument(1),
myTransactionService,
ourCtx,
myInterceptorBroadcaster,
mySearchBuilderFactory,
mySearchResultCacheSvc,
myStorageSettings,
mySearchCacheSvc,
pagingProvider,
myExceptionSvc
);
}
default -> {
fail("Invalid bean type: " + type);
return null;
}
}
});
}
public static class FailAfterNIterator extends BaseIterator<JpaPid> implements IResultIterator<JpaPid> {
private int myCount;
private final IResultIterator<JpaPid> myWrap;
private int myCount;
FailAfterNIterator(IResultIterator theWrap, int theCount) {
myWrap = theWrap;
@ -738,45 +784,4 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
// nothing
}
}
private void mockSearchTask() {
IPagingProvider pagingProvider = mock(IPagingProvider.class);
lenient().when(pagingProvider.getMaximumPageSize())
.thenReturn(500);
when(myBeanFactory.getBean(anyString(), any(SearchTaskParameters.class)))
.thenAnswer(invocation -> {
String type = invocation.getArgument(0);
switch (type) {
case SearchConfig.SEARCH_TASK:
return new SearchTask(
invocation.getArgument(1),
myTransactionService,
ourCtx,
myInterceptorBroadcaster,
mySearchBuilderFactory,
mySearchResultCacheSvc,
myStorageSettings,
mySearchCacheSvc,
pagingProvider
);
case SearchConfig.CONTINUE_TASK:
return new SearchContinuationTask(
invocation.getArgument(1),
myTransactionService,
ourCtx,
myInterceptorBroadcaster,
mySearchBuilderFactory,
mySearchResultCacheSvc,
myStorageSettings,
mySearchCacheSvc,
pagingProvider,
myExceptionSvc
);
default:
fail("Invalid bean type: " + type);
return null;
}
});
}
}

View File

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

View File

@ -2553,17 +2553,16 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
p.addName().setFamily("testMetaAddInvalid");
IIdType id = myClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
//@formatter:off
String input = "<Parameters>\n" +
" <meta>\n" +
" <tag>\n" +
" <system value=\"http://example.org/codes/tags\"/>\n" +
" <code value=\"record-lost\"/>\n" +
" <display value=\"Patient File Lost\"/>\n" +
" </tag>\n" +
" </meta>\n" +
"</Parameters>";
//@formatter:on
String input = """
<Parameters>
<meta>
<tag>
<system value="http://example.org/codes/tags"/>
<code value="record-lost"/>
<display value="Patient File Lost"/>
</tag>
</meta>
</Parameters>""";
HttpPost post = new HttpPost(myServerBase + "/Patient/" + id.getIdPart() + "/$meta-add");
post.setEntity(new StringEntity(input, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));

View File

@ -103,7 +103,7 @@ public class ResourceProviderDstu3ValueSetVersionedTest extends BaseResourceProv
myExtensionalCsId_v1 = myCodeSystemDao.create(theCodeSystem, mySrd).getId().toUnqualifiedVersionless();
}
});
myCodeSystemDao.readEntity(myExtensionalCsId_v1, null).getId();
myCodeSystemDao.readEntity(myExtensionalCsId_v1, null);
theCodeSystem.setId("CodeSystem/cs2");
theCodeSystem.setVersion("2");
@ -116,7 +116,7 @@ public class ResourceProviderDstu3ValueSetVersionedTest extends BaseResourceProv
myExtensionalCsId_v2 = myCodeSystemDao.create(theCodeSystem, mySrd).getId().toUnqualifiedVersionless();
}
});
myCodeSystemDao.readEntity(myExtensionalCsId_v2, null).getId();
myCodeSystemDao.readEntity(myExtensionalCsId_v2, null);
}
@ -126,13 +126,13 @@ public class ResourceProviderDstu3ValueSetVersionedTest extends BaseResourceProv
valueSet.setId("ValueSet/vs1");
valueSet.getCompose().getInclude().get(0).setVersion("1");
myExtensionalVsId_v1 = persistSingleValueSet(valueSet, HttpVerb.POST);
myExtensionalVsIdOnResourceTable_v1 = myValueSetDao.readEntity(myExtensionalVsId_v1, null).getId();
myExtensionalVsIdOnResourceTable_v1 = myValueSetDao.readEntity(myExtensionalVsId_v1, null).getIdDt().getIdPartAsLong();
valueSet.setVersion("2");
valueSet.setId("ValueSet/vs2");
valueSet.getCompose().getInclude().get(0).setVersion("2");
myExtensionalVsId_v2 = persistSingleValueSet(valueSet, HttpVerb.POST);
myExtensionalVsIdOnResourceTable_v2 = myValueSetDao.readEntity(myExtensionalVsId_v2, null).getId();
myExtensionalVsIdOnResourceTable_v2 = myValueSetDao.readEntity(myExtensionalVsId_v2, null).getIdDt().getIdPartAsLong();
}

View File

@ -0,0 +1,46 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n</pattern>
</encoder>
</appender>
<!-- define the root first, so the rest can inherit our logger -->
<root level="info">
<appender-ref ref="STDOUT" />
</root>
<logger name="ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionMatchingSubscriber" level="info"/>
<logger name="org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator" level="info"/>
<logger name="ca.uhn.fhir.jpa.dao.FhirResourceDaoSubscriptionDstu2" level="info"/>
<logger name="org.eclipse.jetty.websocket" level="info"/>
<logger name="org.hibernate.event.internal.DefaultPersistEventListener" level="info"/>
<logger name="org.eclipse" level="error"/>
<logger name="ca.uhn.fhir.rest.client" level="info"/>
<logger name="ca.uhn.fhir.jpa.dao" level="info"/>
<!-- set to debug to enable term expansion logs -->
<logger name="ca.uhn.fhir.jpa.term" level="info"/>
<!-- Set to 'trace' to enable SQL logging -->
<logger name="org.hibernate.SQL" level="info"/>
<!-- Set to 'trace' to enable SQL Value logging -->
<logger name="org.hibernate.type" level="info"/>
<logger name="org.springframework.test.context.cache" level="info"/>
<logger name="ca.uhn.fhir.jpa.bulk" level="info"/>
<!-- more debugging -->
<!--
<logger name="org.elasticsearch.client" level="trace"/>
<logger name="org.hibernate.search.elasticsearch.request" level="TRACE"/>
<logger name="ca.uhn.fhir.jpa.model.search" level="debug"/>
<logger name="org.elasticsearch.client" level="trace"/>
<logger name="org.hibernate.search" level="debug"/>
<logger name="org.hibernate.search.query" level="TRACE"/>
<logger name="org.hibernate.search.elasticsearch.request" level="TRACE"/>
-->
<!-- See https://docs.jboss.org/hibernate/stable/search/reference/en-US/html_single/#backend-lucene-io-writer-infostream for lucene logging
<logger name="org.hibernate.search.backend.lucene.infostream" level="TRACE"/> -->
</configuration>

View File

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

View File

@ -88,10 +88,14 @@ public class Batch2CoordinatorIT extends BaseJpaR4Test {
return RunOutcome.SUCCESS;
}
@Override
@BeforeEach
public void before() {
public void before() throws Exception {
super.before();
myCompletionHandler = details -> {};
myWorkChannel = (LinkedBlockingChannel) myChannelFactory.getOrCreateReceiver(CHANNEL_NAME, JobWorkNotificationJsonMessage.class, new ChannelConsumerSettings());
myStorageSettings.setJobFastTrackingEnabled(true);
}
@AfterEach

View File

@ -17,9 +17,9 @@ import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.apache.ResourceEntity;
import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.test.utilities.HttpClientExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.JsonUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets;
@ -28,20 +28,14 @@ import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.InstantType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@ -54,12 +48,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
@ -89,53 +83,19 @@ public class BulkDataExportProviderTest {
private static final Logger ourLog = LoggerFactory.getLogger(BulkDataExportProviderTest.class);
private static final String GROUP_ID = "Group/G2401";
private static final String G_JOB_ID = "0000000-GGGGGG";
private Server myServer;
@Spy
private final FhirContext myCtx = FhirContext.forR4Cached();
private int myPort;
@RegisterExtension
private final HttpClientExtension myClient = new HttpClientExtension();
@Mock
private IBatch2JobRunner myJobRunner;
private JpaStorageSettings myStorageSettings;
private DaoRegistry myDaoRegistry;
private CloseableHttpClient myClient;
@InjectMocks
private BulkDataExportProvider myProvider;
private RestfulServer servlet;
static Stream<Arguments> paramsProvider(){
return Stream.of(
Arguments.arguments(true),
Arguments.arguments(false)
);
}
@AfterEach
public void after() throws Exception {
JettyUtil.closeServer(myServer);
myClient.close();
}
@BeforeEach
public void start() throws Exception {
myServer = new Server(0);
ServletHandler proxyHandler = new ServletHandler();
servlet = new RestfulServer(myCtx);
servlet.registerProvider(myProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
myServer.setHandler(proxyHandler);
JettyUtil.startServer(myServer);
myPort = JettyUtil.getPortForStartedServer(myServer);
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
myClient = builder.build();
}
@RegisterExtension
private final RestfulServerExtension myServer = new RestfulServerExtension(myCtx)
.withServer(s -> s.registerProvider(myProvider));
private JpaStorageSettings myStorageSettings;
private DaoRegistry myDaoRegistry;
@BeforeEach
public void injectStorageSettings() {
@ -147,9 +107,9 @@ public class BulkDataExportProviderTest {
}
public void startWithFixedBaseUrl() {
String baseUrl = "http://localhost:" + myPort + "/fixedvalue";
String baseUrl = myServer.getBaseUrl() + "/fixedvalue";
HardcodedServerAddressStrategy hardcodedServerAddressStrategy = new HardcodedServerAddressStrategy(baseUrl);
servlet.setServerAddressStrategy(hardcodedServerAddressStrategy);
myServer.withServer(s -> s.setServerAddressStrategy(hardcodedServerAddressStrategy));
}
private BulkExportParameters verifyJobStart() {
@ -196,7 +156,7 @@ public class BulkDataExportProviderTest {
ourLog.debug(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input));
// test
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
ourLog.info("Request: {}", post);
@ -205,7 +165,7 @@ public class BulkDataExportProviderTest {
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
BulkExportParameters params = verifyJobStart();
@ -223,7 +183,7 @@ public class BulkDataExportProviderTest {
.thenReturn(createJobStartResponse());
Parameters input = new Parameters();
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
@ -243,7 +203,7 @@ public class BulkDataExportProviderTest {
InstantType now = InstantType.now();
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON)
+ "&" + JpaConstants.PARAM_EXPORT_TYPE + "=" + UrlUtil.escapeUrlParam("Patient, Practitioner")
+ "&" + JpaConstants.PARAM_EXPORT_SINCE + "=" + UrlUtil.escapeUrlParam(now.getValueAsString())
@ -257,7 +217,7 @@ public class BulkDataExportProviderTest {
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
BulkExportParameters params = verifyJobStart();
@ -272,7 +232,7 @@ public class BulkDataExportProviderTest {
when(myJobRunner.startNewJob(any()))
.thenReturn(createJobStartResponse());
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON)
+ "&" + JpaConstants.PARAM_EXPORT_TYPE + "=" + UrlUtil.escapeUrlParam("Patient,EpisodeOfCare")
+ "&" + JpaConstants.PARAM_EXPORT_TYPE_FILTER + "=" + UrlUtil.escapeUrlParam("Patient?_id=P999999990")
@ -286,7 +246,7 @@ public class BulkDataExportProviderTest {
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
BulkExportParameters params = verifyJobStart();
@ -309,7 +269,7 @@ public class BulkDataExportProviderTest {
.thenReturn(info);
// test
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
@ -338,7 +298,7 @@ public class BulkDataExportProviderTest {
.thenReturn(info);
// call
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
@ -382,7 +342,7 @@ public class BulkDataExportProviderTest {
.thenReturn(info);
// call
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
@ -398,11 +358,11 @@ public class BulkDataExportProviderTest {
BulkExportResponseJson responseJson = JsonUtil.deserialize(responseContent, BulkExportResponseJson.class);
assertEquals(3, responseJson.getOutput().size());
assertEquals("Patient", responseJson.getOutput().get(0).getType());
assertEquals("http://localhost:" + myPort + "/Binary/111", responseJson.getOutput().get(0).getUrl());
assertEquals(myServer.getBaseUrl() + "/Binary/111", responseJson.getOutput().get(0).getUrl());
assertEquals("Patient", responseJson.getOutput().get(1).getType());
assertEquals("http://localhost:" + myPort + "/Binary/222", responseJson.getOutput().get(1).getUrl());
assertEquals(myServer.getBaseUrl() + "/Binary/222", responseJson.getOutput().get(1).getUrl());
assertEquals("Patient", responseJson.getOutput().get(2).getType());
assertEquals("http://localhost:" + myPort + "/Binary/333", responseJson.getOutput().get(2).getUrl());
assertEquals(myServer.getBaseUrl() + "/Binary/333", responseJson.getOutput().get(2).getUrl());
}
}
@ -427,7 +387,7 @@ public class BulkDataExportProviderTest {
.thenReturn(info);
// test
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
@ -453,7 +413,7 @@ public class BulkDataExportProviderTest {
when(myJobRunner.getJobInfo(anyString()))
.thenThrow(new ResourceNotFoundException("Unknown job: AAA"));
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
@ -470,7 +430,7 @@ public class BulkDataExportProviderTest {
/**
* Group export tests
* See https://build.fhir.org/ig/HL7/us-bulk-data/
* See <a href="https://build.fhir.org/ig/HL7/us-bulk-data/">Bulk Data IG</a>
* <p>
* GET [fhir base]/Group/[id]/$export
* <p>
@ -496,7 +456,7 @@ public class BulkDataExportProviderTest {
ourLog.debug(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input));
// call
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + GROUP_ID + "/" + JpaConstants.OPERATION_EXPORT);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + GROUP_ID + "/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
ourLog.info("Request: {}", post);
@ -504,7 +464,7 @@ public class BulkDataExportProviderTest {
ourLog.info("Response: {}", response.toString());
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + G_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + G_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
// verify
@ -525,7 +485,7 @@ public class BulkDataExportProviderTest {
InstantType now = InstantType.now();
String url = "http://localhost:" + myPort + "/" + GROUP_ID + "/" + JpaConstants.OPERATION_EXPORT
String url = myServer.getBaseUrl() + "/" + GROUP_ID + "/" + JpaConstants.OPERATION_EXPORT
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON)
+ "&" + JpaConstants.PARAM_EXPORT_TYPE + "=" + UrlUtil.escapeUrlParam("Patient, Practitioner")
+ "&" + JpaConstants.PARAM_EXPORT_SINCE + "=" + UrlUtil.escapeUrlParam(now.getValueAsString())
@ -541,7 +501,7 @@ public class BulkDataExportProviderTest {
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + G_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + G_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
BulkExportParameters bp = verifyJobStart();
@ -559,7 +519,7 @@ public class BulkDataExportProviderTest {
InstantType now = InstantType.now();
// manual construct
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON)
+ "&" + JpaConstants.PARAM_EXPORT_TYPE + "=" + UrlUtil.escapeUrlParam("Immunization, Observation")
+ "&" + JpaConstants.PARAM_EXPORT_SINCE + "=" + UrlUtil.escapeUrlParam(now.getValueAsString());
@ -567,45 +527,45 @@ public class BulkDataExportProviderTest {
String immunizationTypeFilter1 = "Immunization?patient.identifier=SC378274-MRN|009999997,SC378274-MRN|009999998,SC378274-MRN|009999999&date=2020-01-02";
String immunizationTypeFilter2 = "Immunization?patient=Patient/123";
String observationFilter1 = "Observation?subject=Patient/123&created=ge2020-01-01";
StringBuilder multiValuedTypeFilterBuilder = new StringBuilder()
.append("&")
.append(JpaConstants.PARAM_EXPORT_TYPE_FILTER)
.append("=")
.append(UrlUtil.escapeUrlParam(immunizationTypeFilter1))
.append(",")
.append(UrlUtil.escapeUrlParam(immunizationTypeFilter2))
.append(",")
.append(UrlUtil.escapeUrlParam(observationFilter1));
String multiValuedTypeFilterBuilder = "&" +
JpaConstants.PARAM_EXPORT_TYPE_FILTER +
"=" +
UrlUtil.escapeUrlParam(immunizationTypeFilter1) +
"," +
UrlUtil.escapeUrlParam(immunizationTypeFilter2) +
"," +
UrlUtil.escapeUrlParam(observationFilter1);
url += multiValuedTypeFilterBuilder.toString();
url += multiValuedTypeFilterBuilder;
// call
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
myClient.execute(get);
try (CloseableHttpResponse ignored = myClient.execute(get)) {
// verify
BulkExportParameters bp = verifyJobStart();
assertThat(bp.getFilters(), containsInAnyOrder(immunizationTypeFilter1, immunizationTypeFilter2, observationFilter1));
}
}
@Test
public void testInitiateGroupExportWithInvalidResourceTypesFails() throws IOException {
// when
String url = "http://localhost:" + myPort + "/" + "Group/123/" + JpaConstants.OPERATION_EXPORT
String url = myServer.getBaseUrl() + "/" + "Group/123/" + JpaConstants.OPERATION_EXPORT
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON)
+ "&" + JpaConstants.PARAM_EXPORT_TYPE + "=" + UrlUtil.escapeUrlParam("StructureDefinition,Observation");
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
CloseableHttpResponse execute = myClient.execute(get);
String responseBody = IOUtils.toString(execute.getEntity().getContent());
try (CloseableHttpResponse execute = myClient.execute(get)) {
String responseBody = IOUtils.toString(execute.getEntity().getContent(), StandardCharsets.UTF_8);
// verify
assertThat(execute.getStatusLine().getStatusCode(), is(equalTo(400)));
assertThat(responseBody, is(containsString("Resource types [StructureDefinition] are invalid for this type of export, as they do not contain search parameters that refer to patients.")));
}
}
@Test
public void testInitiateGroupExportWithNoResourceTypes() throws IOException {
@ -614,12 +574,12 @@ public class BulkDataExportProviderTest {
.thenReturn(createJobStartResponse());
// test
String url = "http://localhost:" + myPort + "/" + "Group/123/" + JpaConstants.OPERATION_EXPORT
String url = myServer.getBaseUrl() + "/" + "Group/123/" + JpaConstants.OPERATION_EXPORT
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON);
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
CloseableHttpResponse execute = myClient.execute(get);
try (CloseableHttpResponse execute = myClient.execute(get)) {
// verify
assertThat(execute.getStatusLine().getStatusCode(), is(equalTo(202)));
@ -631,6 +591,7 @@ public class BulkDataExportProviderTest {
() -> assertTrue(bulkExportParameters.getResourceTypes().contains("Device"))
);
}
}
@Test
public void testInitiateWithPostAndMultipleTypeFilters() throws IOException {
@ -645,7 +606,7 @@ public class BulkDataExportProviderTest {
ourLog.debug(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input));
// call
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
ourLog.info("Request: {}", post);
@ -654,7 +615,7 @@ public class BulkDataExportProviderTest {
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
// verify
@ -674,7 +635,7 @@ public class BulkDataExportProviderTest {
input.addParameter(JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT, new StringType(Constants.CT_FHIR_NDJSON));
// call
HttpPost post = new HttpPost("http://localhost:" + myPort + "/Patient/" + JpaConstants.OPERATION_EXPORT);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/Patient/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
ourLog.info("Request: {}", post);
@ -682,7 +643,7 @@ public class BulkDataExportProviderTest {
ourLog.info("Response: {}", response.toString());
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
BulkExportParameters bp = verifyJobStart();
@ -707,7 +668,7 @@ public class BulkDataExportProviderTest {
ourLog.debug(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input));
// call
HttpPost post = new HttpPost("http://localhost:" + myPort + "/Patient/" + JpaConstants.OPERATION_EXPORT);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/Patient/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
ourLog.info("Request: {}", post);
@ -715,7 +676,7 @@ public class BulkDataExportProviderTest {
ourLog.info("Response: {}", response.toString());
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
BulkExportParameters bp = verifyJobStart();
@ -740,7 +701,7 @@ public class BulkDataExportProviderTest {
input.addParameter(JpaConstants.PARAM_EXPORT_TYPE, new StringType("Patient, Practitioner"));
// call
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.addHeader(Constants.HEADER_CACHE_CONTROL, Constants.CACHE_CONTROL_NO_CACHE);
post.setEntity(new ResourceEntity(myCtx, input));
@ -749,7 +710,7 @@ public class BulkDataExportProviderTest {
ourLog.info("Response: {}", response.toString());
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
// verify
@ -757,7 +718,6 @@ public class BulkDataExportProviderTest {
assertThat(parameters.isUseExistingJobsFirst(), is(equalTo(false)));
}
@Test
public void testProvider_whenEnableBatchJobReuseIsFalse_startsNewJob() throws IOException {
// setup
@ -775,7 +735,7 @@ public class BulkDataExportProviderTest {
input.addParameter(JpaConstants.PARAM_EXPORT_TYPE, new StringType("Patient, Practitioner"));
// call
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
ourLog.info("Request: {}", post);
@ -783,7 +743,7 @@ public class BulkDataExportProviderTest {
ourLog.info("Response: {}", response.toString());
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
// verify
@ -791,7 +751,6 @@ public class BulkDataExportProviderTest {
assertThat(parameters.isUseExistingJobsFirst(), is(equalTo(false)));
}
@Test
public void testProviderReturnsSameIdForSameJob() throws IOException {
// given
@ -831,7 +790,7 @@ public class BulkDataExportProviderTest {
.thenReturn(result);
// call
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpDelete delete = new HttpDelete(url);
try (CloseableHttpResponse response = myClient.execute(delete)) {
@ -860,7 +819,7 @@ public class BulkDataExportProviderTest {
.thenReturn(info);
// call
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
String url = myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS + "?" +
JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID + "=" + A_JOB_ID;
HttpDelete delete = new HttpDelete(url);
try (CloseableHttpResponse response = myClient.execute(delete)) {
@ -884,7 +843,7 @@ public class BulkDataExportProviderTest {
.thenReturn(createJobStartResponse());
// call
final HttpGet httpGet = new HttpGet(String.format("http://localhost:%s/%s", myPort, JpaConstants.OPERATION_EXPORT));
final HttpGet httpGet = new HttpGet(String.format("http://localhost:%s/%s", myServer.getPort(), JpaConstants.OPERATION_EXPORT));
httpGet.addHeader("_outputFormat", Constants.CT_FHIR_NDJSON);
httpGet.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
@ -892,7 +851,7 @@ public class BulkDataExportProviderTest {
ourLog.info("Response: {}", response.toString());
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals(String.format("http://localhost:%s/$export-poll-status?_jobId=%s", myPort, A_JOB_ID), response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(String.format("http://localhost:%s/$export-poll-status?_jobId=%s", myServer.getPort(), A_JOB_ID), response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertTrue(IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8).isEmpty());
}
@ -907,14 +866,14 @@ public class BulkDataExportProviderTest {
.thenReturn(createJobStartResponse());
// call
final HttpGet httpGet = new HttpGet(String.format("http://localhost:%s/%s?_outputFormat=%s", myPort, JpaConstants.OPERATION_EXPORT, Constants.CT_FHIR_NDJSON));
final HttpGet httpGet = new HttpGet(String.format("http://localhost:%s/%s?_outputFormat=%s", myServer.getPort(), JpaConstants.OPERATION_EXPORT, Constants.CT_FHIR_NDJSON));
httpGet.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
try (CloseableHttpResponse response = myClient.execute(httpGet)) {
assertAll(
() -> assertEquals(202, response.getStatusLine().getStatusCode()),
() -> assertEquals("Accepted", response.getStatusLine().getReasonPhrase()),
() -> assertEquals(String.format("http://localhost:%s/$export-poll-status?_jobId=%s", myPort, A_JOB_ID), response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue()),
() -> assertEquals(String.format("http://localhost:%s/$export-poll-status?_jobId=%s", myServer.getPort(), A_JOB_ID), response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue()),
() -> assertTrue(IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8).isEmpty())
);
}
@ -933,7 +892,7 @@ public class BulkDataExportProviderTest {
input.addParameter(JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID, new StringType(jobId));
// Initiate Export Poll Status
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
@ -962,7 +921,7 @@ public class BulkDataExportProviderTest {
input.addParameter(JpaConstants.PARAM_EXPORT_POLL_STATUS_JOB_ID, new StringType(A_JOB_ID));
// Initiate Export Poll Status
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
@ -980,7 +939,7 @@ public class BulkDataExportProviderTest {
input.addParameter(JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT, new StringType(ca.uhn.fhir.rest.api.Constants.CT_FHIR_NDJSON));
// Initiate Export Poll Status
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS);
HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT_POLL_STATUS);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input));
@ -992,7 +951,7 @@ public class BulkDataExportProviderTest {
private void callExportAndAssertJobId(Parameters input, String theExpectedJobId) throws IOException {
HttpPost post;
post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
post = new HttpPost(myServer.getBaseUrl() + "/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.addHeader(Constants.HEADER_CACHE_CONTROL, Constants.CACHE_CONTROL_NO_CACHE);
post.setEntity(new ResourceEntity(myCtx, input));
@ -1001,9 +960,16 @@ public class BulkDataExportProviderTest {
ourLog.info("Response: {}", response.toString());
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + theExpectedJobId, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
assertEquals(myServer.getBaseUrl() + "/$export-poll-status?_jobId=" + theExpectedJobId, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
}
static Stream<Arguments> paramsProvider() {
return Stream.of(
Arguments.arguments(true),
Arguments.arguments(false)
);
}
}

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