diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index d2c17731195..4eb78edfb61 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -37,31 +37,7 @@ - - - - org.ow2.asm - asm-all - 5.0.4 - - org.basepom.maven diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java index 410e995d9f5..f8518e3a3f0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java @@ -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 thePartitionIds) { - return new RequestPartitionId(null, toListOrNull(thePartitionIds), null); + return fromPartitionIds(thePartitionIds, null); + } + + @Nonnull + public static RequestPartitionId fromPartitionIds(@Nonnull Collection thePartitionIds, @Nullable LocalDate thePartitionDate) { + return new RequestPartitionId(null, toListOrNull(thePartitionIds), thePartitionDate); } @Nonnull diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index fdff9a69951..571dd26a636 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -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} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2407-allow-partition-date-for-nonpartitionable.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2407-allow-partition-date-for-nonpartitionable.yaml new file mode 100644 index 00000000000..b8942a6f558 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2407-allow-partition-date-for-nonpartitionable.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md index b7806c5713b..2bb299f0bcd 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md @@ -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. diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index ebd15af2a7e..37da051117a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -50,7 +50,7 @@ import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.hasHooks; public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { - private final HashSet myPartitioningBlacklist; + private final HashSet 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); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/PartitioningInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/PartitioningInterceptorR4Test.java index 156f2528082..b2fe10e4e25 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/PartitioningInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/PartitioningInterceptorR4Test.java @@ -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 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 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 */ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java index 2e33c901ee0..94289a2c532 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java @@ -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() { diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java index a5daef31adb..ee503727c3e 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java @@ -144,7 +144,7 @@ public interface ITestDataBuilder { return createResource("Organization", theModifiers); } - default IIdType createResource(String theResourceType, Consumer[] theModifiers) { + default IIdType createResource(String theResourceType, Consumer... theModifiers) { IBaseResource resource = buildResource(theResourceType, theModifiers); if (isNotBlank(resource.getIdElement().getValue())) { diff --git a/pom.xml b/pom.xml index 6a0ec668486..e65670c96e1 100644 --- a/pom.xml +++ b/pom.xml @@ -1958,7 +1958,7 @@ org.codehaus.mojo animal-sniffer-maven-plugin - 1.19 + 1.20 org.codehaus.mojo