diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/PartitionId.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/PartitionId.java index 3139802bbe9..55872f11ba8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/PartitionId.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/PartitionId.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.interceptor.model; * #L% */ +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.time.LocalDate; @@ -27,17 +28,23 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; public class PartitionId { - private Integer myPartitionId; - private LocalDate myPartitionDate; + private final Integer myPartitionId; + private final LocalDate myPartitionDate; + private final String myPartitionName; /** * Constructor */ - public PartitionId(@Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { + private PartitionId(@Nullable String thePartitionName, @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { + myPartitionName = thePartitionName; myPartitionId = thePartitionId; myPartitionDate = thePartitionDate; } + public String getPartitionName() { + return myPartitionName; + } + @Nullable public Integer getPartitionId() { return myPartitionId; @@ -71,4 +78,29 @@ public class PartitionId { return retVal; } + @Nonnull + public static PartitionId forPartitionId(@Nullable Integer thePartitionId) { + return forPartitionId(thePartitionId, null); + } + + @Nonnull + public static PartitionId forPartitionId(@Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { + return new PartitionId(null, thePartitionId, thePartitionDate); + } + + @Nonnull + public static PartitionId forPartitionName(@Nullable String thePartitionName) { + return forPartitionName(thePartitionName, null); + } + + @Nonnull + public static PartitionId forPartitionName(@Nullable String thePartitionName, @Nullable LocalDate thePartitionDate) { + return new PartitionId(thePartitionName, null, thePartitionDate); + } + + @Nonnull + public static PartitionId forPartitionNameAndId(@Nullable String thePartitionName, @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { + return new PartitionId(thePartitionName, thePartitionId, thePartitionDate); + } + } 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 1268104b313..6f60827bafa 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 @@ -141,6 +141,7 @@ ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils.failedToApplyPatch=Failed to apply ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.blacklistedResourceTypeForPartitioning=Resource type {0} can not be partitioned ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.unknownPartitionId=Unknown partition ID: {0} +ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.unknownPartitionName=Unknown partition name: {0} ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference.invalidTargetTypeForChain=Resource type "{0}" is not a valid target type for reference search parameter: {1} @@ -157,4 +158,3 @@ ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantDeleteDefaultPartition=Can ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantRenameDefaultPartition=Can not rename default partition ca.uhn.fhir.jpa.partition.RequestTenantPartitionInterceptor.unknownTenantName=Unknown tenant: {0} - diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md index 85e09283db1..9eea40449d5 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md @@ -46,7 +46,7 @@ This interceptor can implement the hooks shown below. ## Identify Partition for Create (Required) -A hook against the [Pointcut#STORAGE_PARTITION_IDENTIFY_CREATE](/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PARTITION_IDENTIFY_CREATE) pointcut must be registered, and this hook method will be invoked every time a resource is being created in order to determine the partition to create the resource in. +A hook against the [`Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE`](/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PARTITION_IDENTIFY_CREATE) pointcut must be registered, and this hook method will be invoked every time a resource is being created in order to determine the partition to create the resource in. The criteria for determining the partition will depend on your use case. For example: @@ -56,7 +56,9 @@ The criteria for determining the partition will depend on your use case. For exa ## Identify Partition for Read (Optional) -A hook against the [Pointcut#STORAGE_PARTITION_IDENTIFY_CREATE](/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PARTITION_IDENTIFY_CREATE) pointcut must be registered, and this hook method will be invoked every time a resource is being created in order to determine the partition to create the resource in. +A hook against the [`Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE`](/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PARTITION_IDENTIFY_CREATE) pointcut must be registered, and this hook method will be invoked every time a resource is being created in order to determine the partition to create the resource in. + +## Example: Using Request Tenants diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperService.java index 93757468463..c2006ab4458 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperService.java @@ -26,11 +26,14 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.interceptor.model.PartitionId; +import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.model.config.PartitionConfig; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; @@ -94,7 +97,7 @@ public class RequestPartitionHelperService implements IRequestPartitionHelperSer validatePartition(partitionId, theResourceType); } - return partitionId; + return normalize(partitionId); } /** @@ -117,7 +120,51 @@ public class RequestPartitionHelperService implements IRequestPartitionHelperSer validatePartition(partitionId, resourceName); } - return partitionId; + return normalize(partitionId); + } + + /** + * If the partition only has a name but not an ID, this method resolves the ID + * @param thePartitionId + * @return + */ + private PartitionId normalize(PartitionId thePartitionId) { + if (thePartitionId != null) { + if (thePartitionId.getPartitionName() != null) { + + PartitionEntity partition; + try { + partition = myPartitionConfigSvc.getPartitionByName(thePartitionId.getPartitionName()); + } catch (IllegalArgumentException e) { + String msg = myFhirContext.getLocalizer().getMessage(RequestPartitionHelperService.class, "unknownPartitionName", thePartitionId.getPartitionName()); + throw new ResourceNotFoundException(msg); + } + + if (thePartitionId.getPartitionId() != null) { + Validate.isTrue(thePartitionId.getPartitionId().equals(partition.getId()), "Partition name %s does not match ID %n", thePartitionId.getPartitionName(), thePartitionId.getPartitionId()); + return thePartitionId; + } else { + return PartitionId.forPartitionNameAndId(thePartitionId.getPartitionName(), partition.getId(), thePartitionId.getPartitionDate()); + } + } + + if (thePartitionId.getPartitionId() != null) { + PartitionEntity partition; + try { + partition = myPartitionConfigSvc.getPartitionById(thePartitionId.getPartitionId()); + } catch (IllegalArgumentException e) { + String msg = myFhirContext.getLocalizer().getMessage(RequestPartitionHelperService.class, "unknownPartitionId", thePartitionId.getPartitionId()); + throw new ResourceNotFoundException(msg); + } + return PartitionId.forPartitionNameAndId(partition.getName(), partition.getId(), thePartitionId.getPartitionDate()); + } + + } + + // It's still possible that the partition only has a date but no name/id, + // or it could just be null + return thePartitionId; + } private void validatePartition(@Nullable PartitionId thePartitionId, @Nonnull String theResourceName) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantPartitionInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantPartitionInterceptor.java index 7c41d66e819..649f34d94b1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantPartitionInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantPartitionInterceptor.java @@ -20,49 +20,43 @@ package ca.uhn.fhir.jpa.partition; * #L% */ -import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; import ca.uhn.fhir.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.entity.PartitionEntity; -import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.interceptor.model.PartitionId; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import org.hl7.fhir.instance.model.api.IBaseResource; import org.jetbrains.annotations.NotNull; -import org.springframework.beans.factory.annotation.Autowired; +import static org.apache.commons.lang3.StringUtils.isBlank; + +/** + * + */ +@Interceptor public class RequestTenantPartitionInterceptor { - @Autowired - private IPartitionConfigSvc myPartitionConfigSvc; - @Autowired - private FhirContext myFhirContext; - @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE) - public PartitionablePartitionId PartitionIdentifyCreate(IBaseResource theResource, ServletRequestDetails theRequestDetails) { + public PartitionId PartitionIdentifyCreate(IBaseResource theResource, ServletRequestDetails theRequestDetails) { return extractPartitionIdFromRequest(theRequestDetails); } @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ) - public PartitionablePartitionId PartitionIdentifyRead(ServletRequestDetails theRequestDetails) { + public PartitionId PartitionIdentifyRead(ServletRequestDetails theRequestDetails) { return extractPartitionIdFromRequest(theRequestDetails); } @NotNull - private PartitionablePartitionId extractPartitionIdFromRequest(ServletRequestDetails theRequestDetails) { - String tenantId = theRequestDetails.getTenantId(); + private PartitionId extractPartitionIdFromRequest(ServletRequestDetails theRequestDetails) { - PartitionEntity partition; - try { - partition = myPartitionConfigSvc.getPartitionByName(tenantId); - } catch (IllegalArgumentException e) { - String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestTenantPartitionInterceptor.class, "unknownTenantName", tenantId); - throw new ResourceNotFoundException(msg); + // We will use the tenant ID that came from the request as the partition name + String tenantId = theRequestDetails.getTenantId(); + if (isBlank(tenantId)) { + throw new InternalErrorException("No tenant ID has been specified"); } - PartitionablePartitionId retVal = new PartitionablePartitionId(); - retVal.setPartitionId(partition.getId()); - return retVal; + return PartitionId.forPartitionName(tenantId); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java index 5803b564074..fc2108eec2b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java @@ -1695,7 +1695,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { private void addCreatePartition(Integer thePartitionId, LocalDate thePartitionDate) { Validate.notNull(thePartitionId); - PartitionId partitionId = new PartitionId(thePartitionId, thePartitionDate); + PartitionId partitionId = PartitionId.forPartitionId(thePartitionId, thePartitionDate); myPartitionInterceptor.addCreatePartition(partitionId); } @@ -1704,20 +1704,20 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { } private void addCreateNoPartitionId(LocalDate thePartitionDate) { - PartitionId partitionId = new PartitionId(null, thePartitionDate); + PartitionId partitionId = PartitionId.forPartitionId(null, thePartitionDate); myPartitionInterceptor.addCreatePartition(partitionId); } private void addReadPartition(Integer thePartitionId) { PartitionId partitionId = null; if (thePartitionId != null) { - partitionId = new PartitionId(thePartitionId, null); + partitionId = PartitionId.forPartitionId(thePartitionId, null); } myPartitionInterceptor.addReadPartition(partitionId); } private void addDefaultReadPartition() { - PartitionId partitionId = new PartitionId(null, null); + PartitionId partitionId = PartitionId.forPartitionId(null, null); myPartitionInterceptor.addReadPartition(partitionId); } 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 1593da4190e..d4bb03ff99b 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 @@ -123,7 +123,7 @@ public class MultitenantServerR4Test extends BaseResourceProviderR4Test { ourClient.create().resource(patientA).execute(); fail(); } catch (ResourceNotFoundException e) { - assertThat(e.getMessage(), containsString("Unknown tenant: TENANT-ZZZ")); + assertThat(e.getMessage(), containsString("Unknown partition name: TENANT-ZZZ")); } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java index 2cad201f098..27c2e1753b3 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java @@ -81,6 +81,6 @@ public class PartitionablePartitionId implements Cloneable { } public PartitionId toPartitionId() { - return new PartitionId(getPartitionId(), getPartitionDate()); + return PartitionId.forPartitionId(getPartitionId(), getPartitionDate()); } }