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>
|
||||
</configuration>
|
||||
</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>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm-all</artifactId>
|
||||
<version>5.0.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.basepom.maven</groupId>
|
||||
|
|
|
@ -226,6 +226,11 @@ public class RequestPartitionId {
|
|||
return fromPartitionIds(Collections.singletonList(null));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static RequestPartitionId defaultPartition(@Nullable LocalDate thePartitionDate) {
|
||||
return fromPartitionIds(Collections.singletonList(null), thePartitionDate);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId) {
|
||||
return fromPartitionIds(Collections.singletonList(thePartitionId));
|
||||
|
@ -238,7 +243,12 @@ public class RequestPartitionId {
|
|||
|
||||
@Nonnull
|
||||
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
|
||||
|
|
|
@ -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.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.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.
|
||||
|
||||
## 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
|
||||
|
||||
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 {
|
||||
|
||||
private final HashSet<Object> myPartitioningBlacklist;
|
||||
private final HashSet<Object> myNonPartitionableResourceNames;
|
||||
|
||||
@Autowired
|
||||
private IInterceptorBroadcaster myInterceptorBroadcaster;
|
||||
|
@ -62,25 +62,25 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
|||
private PartitionSettings myPartitionSettings;
|
||||
|
||||
public RequestPartitionHelperSvc() {
|
||||
myPartitioningBlacklist = new HashSet<>();
|
||||
myNonPartitionableResourceNames = new HashSet<>();
|
||||
|
||||
// Infrastructure
|
||||
myPartitioningBlacklist.add("Subscription");
|
||||
myPartitioningBlacklist.add("SearchParameter");
|
||||
myNonPartitionableResourceNames.add("Subscription");
|
||||
myNonPartitionableResourceNames.add("SearchParameter");
|
||||
|
||||
// Validation and Conformance
|
||||
myPartitioningBlacklist.add("StructureDefinition");
|
||||
myPartitioningBlacklist.add("Questionnaire");
|
||||
myPartitioningBlacklist.add("CapabilityStatement");
|
||||
myPartitioningBlacklist.add("CompartmentDefinition");
|
||||
myPartitioningBlacklist.add("OperationDefinition");
|
||||
myNonPartitionableResourceNames.add("StructureDefinition");
|
||||
myNonPartitionableResourceNames.add("Questionnaire");
|
||||
myNonPartitionableResourceNames.add("CapabilityStatement");
|
||||
myNonPartitionableResourceNames.add("CompartmentDefinition");
|
||||
myNonPartitionableResourceNames.add("OperationDefinition");
|
||||
|
||||
// Terminology
|
||||
myPartitioningBlacklist.add("ConceptMap");
|
||||
myPartitioningBlacklist.add("CodeSystem");
|
||||
myPartitioningBlacklist.add("ValueSet");
|
||||
myPartitioningBlacklist.add("NamingSystem");
|
||||
myPartitioningBlacklist.add("StructureMap");
|
||||
myNonPartitionableResourceNames.add("ConceptMap");
|
||||
myNonPartitionableResourceNames.add("CodeSystem");
|
||||
myNonPartitionableResourceNames.add("ValueSet");
|
||||
myNonPartitionableResourceNames.add("NamingSystem");
|
||||
myNonPartitionableResourceNames.add("StructureMap");
|
||||
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
|||
|
||||
if (myPartitionSettings.isPartitioningEnabled()) {
|
||||
// Handle system requests
|
||||
if ((theRequest == null && myPartitioningBlacklist.contains(theResourceType))) {
|
||||
if ((theRequest == null && myNonPartitionableResourceNames.contains(theResourceType))) {
|
||||
return RequestPartitionId.defaultPartition();
|
||||
}
|
||||
|
||||
|
@ -128,10 +128,6 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
|||
RequestPartitionId requestPartitionId;
|
||||
|
||||
if (myPartitionSettings.isPartitioningEnabled()) {
|
||||
// Handle system requests
|
||||
if ((theRequest == null && myPartitioningBlacklist.contains(theResourceType))) {
|
||||
return RequestPartitionId.defaultPartition();
|
||||
}
|
||||
|
||||
// Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE
|
||||
HookParams params = new HookParams()
|
||||
|
@ -140,6 +136,12 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
|||
.addIfMatchesType(ServletRequestDetails.class, theRequest);
|
||||
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);
|
||||
validateSinglePartitionForCreate(requestPartitionId, resourceName, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE);
|
||||
|
||||
|
@ -271,8 +273,8 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
|
|||
if ((theRequestPartitionId.hasPartitionIds() && !theRequestPartitionId.getPartitionIds().contains(null)) ||
|
||||
(theRequestPartitionId.hasPartitionNames() && !theRequestPartitionId.getPartitionNames().contains(JpaConstants.DEFAULT_PARTITION_NAME))) {
|
||||
|
||||
if (myPartitioningBlacklist.contains(theResourceName)) {
|
||||
String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperSvc.class, "blacklistedResourceTypeForPartitioning", theResourceName);
|
||||
if (myNonPartitionableResourceNames.contains(theResourceName)) {
|
||||
String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperSvc.class, "nonDefaultPartitionSelectedForNonPartitionable", theResourceName);
|
||||
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.PartitionInterceptorReadPartitionsBasedOnScopes;
|
||||
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.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
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 com.google.common.collect.Sets;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
@ -21,6 +24,7 @@ import org.apache.commons.lang3.Validate;
|
|||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
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.BeforeEach;
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
public void testCreate_InvalidTenant() {
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ public interface ITestDataBuilder {
|
|||
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);
|
||||
|
||||
if (isNotBlank(resource.getIdElement().getValue())) {
|
||||
|
|
Loading…
Reference in New Issue