Allow partition date for non-partitionable resources (#2407)
* Allow partition date for searchparam resources * Cleanup * Test fix * Documentation update * Docs tweak
This commit is contained in:
parent
0e314a9382
commit
b108e43600
|
@ -37,31 +37,7 @@
|
||||||
</signature>
|
</signature>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
<!--
|
|
||||||
<execution>
|
|
||||||
<id>check-android-api</id>
|
|
||||||
<phase>test</phase>
|
|
||||||
<inherited>true</inherited>
|
|
||||||
<goals>
|
|
||||||
<goal>check</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<signature>
|
|
||||||
<groupId>net.sf.androidscents.signature</groupId>
|
|
||||||
<artifactId>android-api-level-21</artifactId>
|
|
||||||
<version>5.0.1_r2</version>
|
|
||||||
</signature>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
-->
|
|
||||||
</executions>
|
</executions>
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ow2.asm</groupId>
|
|
||||||
<artifactId>asm-all</artifactId>
|
|
||||||
<version>5.0.4</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.basepom.maven</groupId>
|
<groupId>org.basepom.maven</groupId>
|
||||||
|
|
|
@ -226,6 +226,11 @@ public class RequestPartitionId {
|
||||||
return fromPartitionIds(Collections.singletonList(null));
|
return fromPartitionIds(Collections.singletonList(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static RequestPartitionId defaultPartition(@Nullable LocalDate thePartitionDate) {
|
||||||
|
return fromPartitionIds(Collections.singletonList(null), thePartitionDate);
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId) {
|
public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId) {
|
||||||
return fromPartitionIds(Collections.singletonList(thePartitionId));
|
return fromPartitionIds(Collections.singletonList(thePartitionId));
|
||||||
|
@ -238,7 +243,12 @@ public class RequestPartitionId {
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public static RequestPartitionId fromPartitionIds(@Nonnull Collection<Integer> thePartitionIds) {
|
public static RequestPartitionId fromPartitionIds(@Nonnull Collection<Integer> thePartitionIds) {
|
||||||
return new RequestPartitionId(null, toListOrNull(thePartitionIds), null);
|
return fromPartitionIds(thePartitionIds, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static RequestPartitionId fromPartitionIds(@Nonnull Collection<Integer> thePartitionIds, @Nullable LocalDate thePartitionDate) {
|
||||||
|
return new RequestPartitionId(null, toListOrNull(thePartitionIds), thePartitionDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
|
|
@ -164,7 +164,7 @@ ca.uhn.fhir.jpa.patch.JsonPatchUtils.failedToApplyPatch=Failed to apply JSON pat
|
||||||
|
|
||||||
ca.uhn.fhir.jpa.graphql.JpaStorageServices.invalidGraphqlArgument=Unknown GraphQL argument "{0}". Value GraphQL argument for this type are: {1}
|
ca.uhn.fhir.jpa.graphql.JpaStorageServices.invalidGraphqlArgument=Unknown GraphQL argument "{0}". Value GraphQL argument for this type are: {1}
|
||||||
|
|
||||||
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.blacklistedResourceTypeForPartitioning=Resource type {0} can not be partitioned
|
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.nonDefaultPartitionSelectedForNonPartitionable=Resource type {0} can not be partitioned
|
||||||
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.unknownPartitionId=Unknown partition ID: {0}
|
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.unknownPartitionId=Unknown partition ID: {0}
|
||||||
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.unknownPartitionName=Unknown partition name: {0}
|
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.unknownPartitionName=Unknown partition name: {0}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
type: add
|
||||||
|
issue: 2407
|
||||||
|
title: "When using the JPA server in partitioned mode with a partition interceptor, the interceptor is now called even for
|
||||||
|
resource types that can not be placed in a non-default partition (e.g. SearchParameter, CodeSystem, etc.). The interceptor
|
||||||
|
may return null or default in this case, but can include a non-null partition date if needed."
|
|
@ -82,6 +82,25 @@ A hook against the [`Pointcut.STORAGE_PARTITION_IDENTIFY_READ`](/hapi-fhir/apido
|
||||||
|
|
||||||
As of HAPI FHIR 5.3.0, the *Identify Partition for Read* hook method may return multiple partition names or IDs. If more than one partition is identified, the server will search in all identified partitions.
|
As of HAPI FHIR 5.3.0, the *Identify Partition for Read* hook method may return multiple partition names or IDs. If more than one partition is identified, the server will search in all identified partitions.
|
||||||
|
|
||||||
|
## Non-Partitionable Resources
|
||||||
|
|
||||||
|
Some resource types can not be placed in any partition other than the DEFAULT partition. When a resource of one of these types is being created, the *STORAGE_PARTITION_IDENTIFY_CREATE* pointcut is invoked, but the hook method must return [defaultPartition()](https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/model/RequestPartitionId.html#defaultPartition()). A partition date may optionally be included.
|
||||||
|
|
||||||
|
The following resource types may not be placed in any partition except the default partition:
|
||||||
|
|
||||||
|
* CapabilityStatement
|
||||||
|
* CodeSystem
|
||||||
|
* CompartmentDefinition
|
||||||
|
* ConceptMap
|
||||||
|
* NamingSystem
|
||||||
|
* OperationDefinition
|
||||||
|
* Questionnaire
|
||||||
|
* SearchParameter
|
||||||
|
* StructureDefinition
|
||||||
|
* StructureMap
|
||||||
|
* Subscription
|
||||||
|
* ValueSet
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
See [Partition Interceptor Examples](./partition_interceptor_examples.html) for various samples of how partitioning interceptors can be set up.
|
See [Partition Interceptor Examples](./partition_interceptor_examples.html) for various samples of how partitioning interceptors can be set up.
|
||||||
|
|
|
@ -50,7 +50,7 @@ import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.hasHooks;
|
||||||
|
|
||||||
public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
||||||
|
|
||||||
private final HashSet<Object> myPartitioningBlacklist;
|
private final HashSet<Object> myNonPartitionableResourceNames;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IInterceptorBroadcaster myInterceptorBroadcaster;
|
private IInterceptorBroadcaster myInterceptorBroadcaster;
|
||||||
|
@ -62,25 +62,25 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
||||||
private PartitionSettings myPartitionSettings;
|
private PartitionSettings myPartitionSettings;
|
||||||
|
|
||||||
public RequestPartitionHelperSvc() {
|
public RequestPartitionHelperSvc() {
|
||||||
myPartitioningBlacklist = new HashSet<>();
|
myNonPartitionableResourceNames = new HashSet<>();
|
||||||
|
|
||||||
// Infrastructure
|
// Infrastructure
|
||||||
myPartitioningBlacklist.add("Subscription");
|
myNonPartitionableResourceNames.add("Subscription");
|
||||||
myPartitioningBlacklist.add("SearchParameter");
|
myNonPartitionableResourceNames.add("SearchParameter");
|
||||||
|
|
||||||
// Validation and Conformance
|
// Validation and Conformance
|
||||||
myPartitioningBlacklist.add("StructureDefinition");
|
myNonPartitionableResourceNames.add("StructureDefinition");
|
||||||
myPartitioningBlacklist.add("Questionnaire");
|
myNonPartitionableResourceNames.add("Questionnaire");
|
||||||
myPartitioningBlacklist.add("CapabilityStatement");
|
myNonPartitionableResourceNames.add("CapabilityStatement");
|
||||||
myPartitioningBlacklist.add("CompartmentDefinition");
|
myNonPartitionableResourceNames.add("CompartmentDefinition");
|
||||||
myPartitioningBlacklist.add("OperationDefinition");
|
myNonPartitionableResourceNames.add("OperationDefinition");
|
||||||
|
|
||||||
// Terminology
|
// Terminology
|
||||||
myPartitioningBlacklist.add("ConceptMap");
|
myNonPartitionableResourceNames.add("ConceptMap");
|
||||||
myPartitioningBlacklist.add("CodeSystem");
|
myNonPartitionableResourceNames.add("CodeSystem");
|
||||||
myPartitioningBlacklist.add("ValueSet");
|
myNonPartitionableResourceNames.add("ValueSet");
|
||||||
myPartitioningBlacklist.add("NamingSystem");
|
myNonPartitionableResourceNames.add("NamingSystem");
|
||||||
myPartitioningBlacklist.add("StructureMap");
|
myNonPartitionableResourceNames.add("StructureMap");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
||||||
|
|
||||||
if (myPartitionSettings.isPartitioningEnabled()) {
|
if (myPartitionSettings.isPartitioningEnabled()) {
|
||||||
// Handle system requests
|
// Handle system requests
|
||||||
if ((theRequest == null && myPartitioningBlacklist.contains(theResourceType))) {
|
if ((theRequest == null && myNonPartitionableResourceNames.contains(theResourceType))) {
|
||||||
return RequestPartitionId.defaultPartition();
|
return RequestPartitionId.defaultPartition();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,10 +128,6 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
||||||
RequestPartitionId requestPartitionId;
|
RequestPartitionId requestPartitionId;
|
||||||
|
|
||||||
if (myPartitionSettings.isPartitioningEnabled()) {
|
if (myPartitionSettings.isPartitioningEnabled()) {
|
||||||
// Handle system requests
|
|
||||||
if ((theRequest == null && myPartitioningBlacklist.contains(theResourceType))) {
|
|
||||||
return RequestPartitionId.defaultPartition();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE
|
// Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE
|
||||||
HookParams params = new HookParams()
|
HookParams params = new HookParams()
|
||||||
|
@ -140,6 +136,12 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
||||||
.addIfMatchesType(ServletRequestDetails.class, theRequest);
|
.addIfMatchesType(ServletRequestDetails.class, theRequest);
|
||||||
requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params);
|
requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params);
|
||||||
|
|
||||||
|
// Handle system requests
|
||||||
|
boolean nonPartitionableResource = myNonPartitionableResourceNames.contains(theResourceType);
|
||||||
|
if (nonPartitionableResource && requestPartitionId == null) {
|
||||||
|
requestPartitionId = RequestPartitionId.defaultPartition();
|
||||||
|
}
|
||||||
|
|
||||||
String resourceName = myFhirContext.getResourceType(theResource);
|
String resourceName = myFhirContext.getResourceType(theResource);
|
||||||
validateSinglePartitionForCreate(requestPartitionId, resourceName, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE);
|
validateSinglePartitionForCreate(requestPartitionId, resourceName, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE);
|
||||||
|
|
||||||
|
@ -271,8 +273,8 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
||||||
if ((theRequestPartitionId.hasPartitionIds() && !theRequestPartitionId.getPartitionIds().contains(null)) ||
|
if ((theRequestPartitionId.hasPartitionIds() && !theRequestPartitionId.getPartitionIds().contains(null)) ||
|
||||||
(theRequestPartitionId.hasPartitionNames() && !theRequestPartitionId.getPartitionNames().contains(JpaConstants.DEFAULT_PARTITION_NAME))) {
|
(theRequestPartitionId.hasPartitionNames() && !theRequestPartitionId.getPartitionNames().contains(JpaConstants.DEFAULT_PARTITION_NAME))) {
|
||||||
|
|
||||||
if (myPartitioningBlacklist.contains(theResourceName)) {
|
if (myNonPartitionableResourceNames.contains(theResourceName)) {
|
||||||
String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperSvc.class, "blacklistedResourceTypeForPartitioning", theResourceName);
|
String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperSvc.class, "nonDefaultPartitionSelectedForNonPartitionable", theResourceName);
|
||||||
throw new UnprocessableEntityException(msg);
|
throw new UnprocessableEntityException(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,13 @@ import ca.uhn.fhir.jpa.entity.PartitionEntity;
|
||||||
import ca.uhn.fhir.jpa.interceptor.ex.PartitionInterceptorReadAllPartitions;
|
import ca.uhn.fhir.jpa.interceptor.ex.PartitionInterceptorReadAllPartitions;
|
||||||
import ca.uhn.fhir.jpa.interceptor.ex.PartitionInterceptorReadPartitionsBasedOnScopes;
|
import ca.uhn.fhir.jpa.interceptor.ex.PartitionInterceptorReadPartitionsBasedOnScopes;
|
||||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||||
import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
|
import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
@ -21,6 +24,7 @@ import org.apache.commons.lang3.Validate;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
import org.hl7.fhir.r4.model.StructureDefinition;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -82,6 +86,51 @@ public class PartitioningInterceptorR4Test extends BaseJpaR4SystemTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateNonPartionableResourceWithPartitionDate() {
|
||||||
|
myPartitionInterceptor.addCreatePartition(RequestPartitionId.defaultPartition(LocalDate.of(2021, 2, 22)));
|
||||||
|
|
||||||
|
StructureDefinition sd = new StructureDefinition();
|
||||||
|
sd.setUrl("http://foo");
|
||||||
|
myStructureDefinitionDao.create(sd);
|
||||||
|
|
||||||
|
runInTransaction(()->{
|
||||||
|
List<ResourceTable> resources = myResourceTableDao.findAll();
|
||||||
|
assertEquals(1, resources.size());
|
||||||
|
assertEquals(null, resources.get(0).getPartitionId().getPartitionId());
|
||||||
|
assertEquals(22, resources.get(0).getPartitionId().getPartitionDate().getDayOfMonth());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateNonPartionableResourceWithNullPartitionReturned() {
|
||||||
|
myPartitionInterceptor.addCreatePartition(null);
|
||||||
|
|
||||||
|
StructureDefinition sd = new StructureDefinition();
|
||||||
|
sd.setUrl("http://foo");
|
||||||
|
myStructureDefinitionDao.create(sd);
|
||||||
|
|
||||||
|
runInTransaction(()->{
|
||||||
|
List<ResourceTable> resources = myResourceTableDao.findAll();
|
||||||
|
assertEquals(1, resources.size());
|
||||||
|
assertEquals(null, resources.get(0).getPartitionId());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateNonPartionableResourceWithDisallowedPartitionReturned() {
|
||||||
|
myPartitionInterceptor.addCreatePartition(RequestPartitionId.fromPartitionName("FOO"));
|
||||||
|
|
||||||
|
StructureDefinition sd = new StructureDefinition();
|
||||||
|
sd.setUrl("http://foo");
|
||||||
|
try {
|
||||||
|
myStructureDefinitionDao.create(sd);
|
||||||
|
fail();
|
||||||
|
} catch (UnprocessableEntityException e) {
|
||||||
|
assertEquals("Resource type StructureDefinition can not be partitioned", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should fail if no interceptor is registered for the READ pointcut
|
* Should fail if no interceptor is registered for the READ pointcut
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -118,6 +118,21 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateAndRead_NonPartitionableResource_DefaultTenant() {
|
||||||
|
|
||||||
|
// Create patients
|
||||||
|
|
||||||
|
IIdType idA = createResource("NamingSystem", withTenant(JpaConstants.DEFAULT_PARTITION_NAME), withStatus("draft"));
|
||||||
|
|
||||||
|
runInTransaction(() -> {
|
||||||
|
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||||
|
assertNull(resourceTable.getPartitionId());
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreate_InvalidTenant() {
|
public void testCreate_InvalidTenant() {
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@ public interface ITestDataBuilder {
|
||||||
return createResource("Organization", theModifiers);
|
return createResource("Organization", theModifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
default IIdType createResource(String theResourceType, Consumer<IBaseResource>[] theModifiers) {
|
default IIdType createResource(String theResourceType, Consumer<IBaseResource>... theModifiers) {
|
||||||
IBaseResource resource = buildResource(theResourceType, theModifiers);
|
IBaseResource resource = buildResource(theResourceType, theModifiers);
|
||||||
|
|
||||||
if (isNotBlank(resource.getIdElement().getValue())) {
|
if (isNotBlank(resource.getIdElement().getValue())) {
|
||||||
|
|
2
pom.xml
2
pom.xml
|
@ -1958,7 +1958,7 @@
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.codehaus.mojo</groupId>
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
<artifactId>animal-sniffer-maven-plugin</artifactId>
|
<artifactId>animal-sniffer-maven-plugin</artifactId>
|
||||||
<version>1.19</version>
|
<version>1.20</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.codehaus.mojo</groupId>
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
|
Loading…
Reference in New Issue