From e55ccf88fc7e90eace40398cbcf87c9fce1ad812 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Fri, 10 Apr 2020 20:01:59 -0400 Subject: [PATCH] Work on multitenancy --- .../java/ca/uhn/fhir/util/ParametersUtil.java | 25 ++- .../ca/uhn/fhir/i18n/hapi-messages.properties | 15 +- .../built_in_server_interceptors.md | 7 + .../ca/uhn/fhir/jpa/config/BaseConfig.java | 17 +- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 2 +- .../fhir/jpa/dao/FulltextSearchSvcImpl.java | 2 +- .../uhn/fhir/jpa/dao/data/IPartitionDao.java | 36 ++++ .../dao/expunge/ExpungeEverythingService.java | 1 + .../uhn/fhir/jpa/entity/PartitionEntity.java | 56 +++++ .../jpa/partition/IPartitionConfigSvc.java | 26 +++ .../jpa/partition/PartitionConfigSvcImpl.java | 204 ++++++++++++++++++ .../PartitionManagementProvider.java | 113 ++++++++++ .../RequestPartitionHelperService.java | 22 +- .../RequestTenantSelectingInterceptor.java | 49 +++++ .../jpa/search/SearchCoordinatorSvcImpl.java | 2 +- .../ca/uhn/fhir/jpa/config/TestR4Config.java | 2 +- .../java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java | 13 ++ .../ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 7 +- .../fhir/jpa/dao/r4/PartitioningR4Test.java | 26 +++ .../partition/PartitionConfigSvcImplTest.java | 173 +++++++++++++++ .../PartitionManagementProviderTest.java | 99 +++++++++ .../tasks/HapiFhirJpaMigrationTasks.java | 7 +- hapi-fhir-jpaserver-model/pom.xml | 4 + .../jpa/model/util/ProviderConstants.java | 14 ++ hapi-fhir-jpaserver-searchparam/pom.xml | 16 ++ .../matcher/InMemoryResourceMatcher.java | 1 - .../uhn/fhir/util/ParametersUtilR4Test.java | 15 ++ pom.xml | 5 + 28 files changed, 940 insertions(+), 19 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IPartitionDao.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/PartitionEntity.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionConfigSvc.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionConfigSvcImpl.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.java rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/{dao => }/partition/RequestPartitionHelperService.java (82%) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantSelectingInterceptor.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionConfigSvcImplTest.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionManagementProviderTest.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java index 42292361c26..a28197931be 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java @@ -30,6 +30,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.function.Function; + +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; /** * Utilities for dealing with parameters resources in a version indepenedent way @@ -37,12 +40,26 @@ import java.util.Optional; public class ParametersUtil { public static List getNamedParameterValuesAsString(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) { + Function, String> mapper = t -> defaultIfBlank(t.getValueAsString(), null); + return extractNamedParameters(theCtx, theParameters, theParameterName, mapper); + } + + public static List getNamedParameterValuesAsInteger(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) { + Function, Integer> mapper = t -> (Integer)t.getValue(); + return extractNamedParameters(theCtx, theParameters, theParameterName, mapper); + } + + public static Optional getNamedParameterValueAsInteger(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) { + return getNamedParameterValuesAsInteger(theCtx, theParameters, theParameterName).stream().findFirst(); + } + + private static List extractNamedParameters(FhirContext theCtx, IBaseParameters theParameters, String theParameterName, Function, T> theMapper) { Validate.notNull(theParameters, "theParameters must not be null"); RuntimeResourceDefinition resDef = theCtx.getResourceDefinition(theParameters.getClass()); BaseRuntimeChildDefinition parameterChild = resDef.getChildByName("parameter"); List parameterReps = parameterChild.getAccessor().getValues(theParameters); - List retVal = new ArrayList<>(); + List retVal = new ArrayList<>(); for (IBase nextParameter : parameterReps) { BaseRuntimeElementCompositeDefinition nextParameterDef = (BaseRuntimeElementCompositeDefinition) theCtx.getElementDefinition(nextParameter.getClass()); @@ -62,12 +79,12 @@ public class ParametersUtil { valueValues .stream() .filter(t -> t instanceof IPrimitiveType) - .map(t -> ((IPrimitiveType) t).getValueAsString()) - .filter(StringUtils::isNotBlank) + .map(t->((IPrimitiveType) t)) + .map(theMapper) + .filter(t -> t != null) .forEach(retVal::add); } - return retVal; } 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 cb308d4cfb0..729ae8cc11a 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 @@ -138,9 +138,22 @@ ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.expansionTooLarge=Expansion of ValueSet ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils.failedToApplyPatch=Failed to apply JSON patch to {0}: {1} -ca.uhn.fhir.jpa.dao.partition.RequestPartitionHelperService.blacklistedResourceTypeForPartitioning=Resource type {0} can not be partitioned +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.dao.predicate.PredicateBuilderReference.invalidTargetTypeForChain=Resource type "{0}" is not a valid target type for reference search parameter: {1} ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference.invalidResourceType=Invalid/unsupported resource type: "{0}" ca.uhn.fhir.jpa.dao.index.IdHelperService.nonUniqueForcedId=Non-unique ID specified, can not process request + +ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.missingPartitionIdOrName=Partition must have an ID and a Name +ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantCreatePartition0=Can not create a partition with ID 0 (this is a reserved value) +ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.unknownPartitionId=No partition exists with ID {0} +ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.invalidName=Partition name "{0}" is not valid +ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantCreateDuplicatePartitionName=Partition name "{0}" is already defined +ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantDeleteDefaultPartition=Can not delete default partition +ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantRenameDefaultPartition=Can not rename default partition + +ca.uhn.fhir.jpa.partition.RequestTenantSelectingInterceptor.unknownTenantName=Unknown tenant: {0} + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md index 67deec6bf9c..4cceda4e377 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md @@ -146,6 +146,13 @@ Some security audit tools require that servers return an HTTP 405 if an unsuppor * [BanUnsupportedHttpMethodsInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/BanUnsupportedHttpMethodsInterceptor.html) * [BanUnsupportedHttpMethodsInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/BanUnsupportedHttpMethodsInterceptor.java) +# Subscription: Subscription Debug Log Interceptor + +When using Subscriptions, the debug log interceptor can be used to add a number of additional lines to the server logs showing the internals of the subscription processing pipeline. + +* [SubscriptionDebugLogInterceptor JavaDoc](/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.html) +* [SubscriptionDebugLogInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master//hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.java) + # Request Pre-Processing: Override Meta.source diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index ab5d08828d5..f0559debd55 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -11,7 +11,10 @@ import ca.uhn.fhir.jpa.bulk.BulkDataExportProvider; import ca.uhn.fhir.jpa.bulk.BulkDataExportSvcImpl; import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc; import ca.uhn.fhir.jpa.dao.ISearchBuilder; -import ca.uhn.fhir.jpa.dao.partition.RequestPartitionHelperService; +import ca.uhn.fhir.jpa.partition.IPartitionConfigSvc; +import ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl; +import ca.uhn.fhir.jpa.partition.PartitionManagementProvider; +import ca.uhn.fhir.jpa.partition.RequestPartitionHelperService; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; @@ -225,6 +228,18 @@ public abstract class BaseConfig { return new JpaConsentContextServices(); } + @Bean + @Lazy + public IPartitionConfigSvc partitionConfigSvc() { + return new PartitionConfigSvcImpl(); + } + + @Bean + @Lazy + public PartitionManagementProvider partitionManagementProvider() { + return new PartitionManagementProvider(); + } + @Bean @Lazy public TerminologyUploaderProvider terminologyUploaderProvider() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index e7ff2d2878c..28009752d43 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -32,7 +32,7 @@ import ca.uhn.fhir.jpa.api.model.DeleteConflictList; import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; -import ca.uhn.fhir.jpa.dao.partition.RequestPartitionHelperService; +import ca.uhn.fhir.jpa.partition.RequestPartitionHelperService; import ca.uhn.fhir.jpa.delete.DeleteConflictService; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.BaseHasResource; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java index dfa313d12ad..68b10061276 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java @@ -23,7 +23,7 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.dao.partition.RequestPartitionHelperService; +import ca.uhn.fhir.jpa.partition.RequestPartitionHelperService; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.PartitionId; import ca.uhn.fhir.jpa.model.entity.ResourceTable; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IPartitionDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IPartitionDao.java new file mode 100644 index 00000000000..d8df1c5fbeb --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IPartitionDao.java @@ -0,0 +1,36 @@ +package ca.uhn.fhir.jpa.dao.data; + +import ca.uhn.fhir.jpa.entity.PartitionEntity; +import org.checkerframework.checker.nullness.Opt; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * 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 interface IPartitionDao extends JpaRepository { + + @Query("SELECT p FROM PartitionEntity p WHERE p.myName = :name") + Optional findForName(@Param("name") String theName); + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java index bcdddb572fc..8d48ec550a2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java @@ -121,6 +121,7 @@ public class ExpungeEverythingService { counter.addAndGet(expungeEverythingByType(ResourceHistoryProvenanceEntity.class)); counter.addAndGet(expungeEverythingByType(ResourceHistoryTable.class)); counter.addAndGet(expungeEverythingByType(ResourceTable.class)); + counter.addAndGet(expungeEverythingByType(PartitionEntity.class)); myTxTemplate.execute(t -> { counter.addAndGet(doExpungeEverythingQuery("DELETE from " + org.hibernate.search.jpa.Search.class.getSimpleName() + " d")); return null; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/PartitionEntity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/PartitionEntity.java new file mode 100644 index 00000000000..31cdf71fb3b --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/PartitionEntity.java @@ -0,0 +1,56 @@ +package ca.uhn.fhir.jpa.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +@Entity +@Table(name = "HFJ_PARTITION", uniqueConstraints = { + @UniqueConstraint(name = "IDX_PART_NAME", columnNames = {"PART_NAME"}) +}) +public class PartitionEntity { + + public static final int MAX_NAME_LENGTH = 200; + public static final int MAX_DESC_LENGTH = 200; + + /** + * Note that unlike most PID columns in HAPI FHIR JPA, this one is an Integer, and isn't + * auto assigned. + */ + @Id + @Column(name = "PART_ID", nullable = false) + private Integer myId; + @Column(name = "PART_NAME", length = MAX_NAME_LENGTH, nullable = false) + private String myName; + @Column(name = "PART_DESC", length = MAX_DESC_LENGTH, nullable = true) + private String myDescription; + + public Integer getId() { + return myId; + } + + public PartitionEntity setId(Integer theId) { + myId = theId; + return this; + } + + public String getName() { + return myName; + } + + public PartitionEntity setName(String theName) { + myName = theName; + return this; + } + + public String getDescription() { + return myDescription; + } + + public void setDescription(String theDescription) { + myDescription = theDescription; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionConfigSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionConfigSvc.java new file mode 100644 index 00000000000..a80de5e0d33 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionConfigSvc.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.jpa.partition; + +import ca.uhn.fhir.jpa.entity.PartitionEntity; + +public interface IPartitionConfigSvc { + + /** + * This is mostly here for unit test purposes. Regular code is not expected to call this method directly. + */ + void start(); + + /** + * @throws IllegalArgumentException If the name is not known + */ + PartitionEntity getPartitionByName(String theName) throws IllegalArgumentException; + + PartitionEntity getPartitionById(Integer theId); + + void clearCaches(); + + PartitionEntity createPartition(PartitionEntity thePartition); + + PartitionEntity updatePartition(PartitionEntity thePartition); + + void deletePartition(Integer thePartitionId); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionConfigSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionConfigSvcImpl.java new file mode 100644 index 00000000000..e00a6ecbcdb --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionConfigSvcImpl.java @@ -0,0 +1,204 @@ +package ca.uhn.fhir.jpa.partition; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.data.IPartitionDao; +import ca.uhn.fhir.jpa.entity.PartitionEntity; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import org.apache.commons.lang3.Validate; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.annotation.PostConstruct; +import javax.transaction.Transactional; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +public class PartitionConfigSvcImpl implements IPartitionConfigSvc { + + public static final int DEFAULT_PERSISTED_PARTITION_ID = 0; + private static final String DEFAULT_PERSISTED_PARTITION_NAME = "DEFAULT"; + private static final String DEFAULT_PERSISTED_PARTITION_DESC = "Default partition"; + private static final Pattern PARTITION_NAME_VALID_PATTERN = Pattern.compile("[a-zA-Z0-9_-]+"); + private static final Logger ourLog = LoggerFactory.getLogger(PartitionConfigSvcImpl.class); + + @Autowired + private PlatformTransactionManager myTxManager; + @Autowired + private IPartitionDao myPartitionDao; + + private LoadingCache myNameToPartitionCache; + private LoadingCache myIdToPartitionCache; + private TransactionTemplate myTxTemplate; + @Autowired + private FhirContext myFhirCtx; + + @Override + @PostConstruct + public void start() { + myNameToPartitionCache = Caffeine + .newBuilder() + .expireAfterWrite(1, TimeUnit.MINUTES) + .build(new NameToPartitionCacheLoader()); + myIdToPartitionCache = Caffeine + .newBuilder() + .expireAfterWrite(1, TimeUnit.MINUTES) + .build(new IdToPartitionCacheLoader()); + myTxTemplate = new TransactionTemplate(myTxManager); + + // Create default partition definition if it doesn't already exist + myTxTemplate.executeWithoutResult(t -> { + if (myPartitionDao.findById(DEFAULT_PERSISTED_PARTITION_ID).isPresent() == false) { + ourLog.info("Creating default partition definition"); + PartitionEntity partitionEntity = new PartitionEntity(); + partitionEntity.setId(DEFAULT_PERSISTED_PARTITION_ID); + partitionEntity.setName(DEFAULT_PERSISTED_PARTITION_NAME); + partitionEntity.setDescription(DEFAULT_PERSISTED_PARTITION_DESC); + myPartitionDao.save(partitionEntity); + } + }); + + } + + @Override + public PartitionEntity getPartitionByName(String theName) { + Validate.notBlank(theName, "The name must not be null or blank"); + return myNameToPartitionCache.get(theName); + } + + @Override + public PartitionEntity getPartitionById(Integer theId) { + Validate.notNull(theId, "The ID must not be null"); + return myIdToPartitionCache.get(theId); + } + + @Override + public void clearCaches() { + myNameToPartitionCache.invalidateAll(); + myIdToPartitionCache.invalidateAll(); + } + + @Override + @Transactional + public PartitionEntity createPartition(PartitionEntity thePartition) { + validateHaveValidPartitionIdAndName(thePartition); + validatePartitionNameDoesntAlreadyExist(thePartition.getName()); + + if (thePartition.getId() == DEFAULT_PERSISTED_PARTITION_ID) { + String msg = myFhirCtx.getLocalizer().getMessage(PartitionConfigSvcImpl.class, "cantCreatePartition0"); + throw new InvalidRequestException(msg); + } + + ourLog.info("Creating new partition with ID {} and Name {}", thePartition.getId(), thePartition.getName()); + + myPartitionDao.save(thePartition); + return thePartition; + } + + @Override + @Transactional + public PartitionEntity updatePartition(PartitionEntity thePartition) { + validateHaveValidPartitionIdAndName(thePartition); + + Optional existingPartitionOpt = myPartitionDao.findById(thePartition.getId()); + if (existingPartitionOpt.isPresent() == false) { + String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionConfigSvcImpl.class, "unknownPartitionId", thePartition.getId()); + throw new InvalidRequestException(msg); + } + + PartitionEntity existingPartition = existingPartitionOpt.get(); + if (!thePartition.getName().equalsIgnoreCase(existingPartition.getName())) { + validatePartitionNameDoesntAlreadyExist(thePartition.getName()); + } + + if (DEFAULT_PERSISTED_PARTITION_ID == thePartition.getId()) { + if (!DEFAULT_PERSISTED_PARTITION_NAME.equals(thePartition.getName())) { + String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionConfigSvcImpl.class, "cantRenameDefaultPartition"); + throw new InvalidRequestException(msg); + } + } + + existingPartition.setName(thePartition.getName()); + existingPartition.setDescription(thePartition.getDescription()); + myPartitionDao.save(existingPartition); + clearCaches(); + return existingPartition; + } + + @Override + @Transactional + public void deletePartition(Integer thePartitionId) { + Validate.notNull(thePartitionId); + + if (DEFAULT_PERSISTED_PARTITION_ID == thePartitionId) { + String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionConfigSvcImpl.class, "cantDeleteDefaultPartition"); + throw new InvalidRequestException(msg); + } + + Optional partition = myPartitionDao.findById(thePartitionId); + if (!partition.isPresent()) { + String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionConfigSvcImpl.class, "unknownPartitionId", thePartitionId); + throw new IllegalArgumentException(msg); + } + + myPartitionDao.delete(partition.get()); + + clearCaches(); + } + + private void validatePartitionNameDoesntAlreadyExist(String theName) { + if (myPartitionDao.findForName(theName).isPresent()) { + String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionConfigSvcImpl.class, "cantCreateDuplicatePartitionName", theName); + throw new InvalidRequestException(msg); + } + } + + private void validateHaveValidPartitionIdAndName(PartitionEntity thePartition) { + if (thePartition.getId() == null || isBlank(thePartition.getName())) { + String msg = myFhirCtx.getLocalizer().getMessage(PartitionConfigSvcImpl.class, "missingPartitionIdOrName"); + throw new InvalidRequestException(msg); + } + + if (!PARTITION_NAME_VALID_PATTERN.matcher(thePartition.getName()).matches()) { + String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionConfigSvcImpl.class, "invalidName", thePartition.getName()); + throw new InvalidRequestException(msg); + } + + } + + private class NameToPartitionCacheLoader implements @NonNull CacheLoader { + @Nullable + @Override + public PartitionEntity load(@NonNull String theName) { + return myTxTemplate.execute(t -> myPartitionDao + .findForName(theName) + .orElseThrow(() -> { + String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionConfigSvcImpl.class, "invalidName", theName); + return new IllegalArgumentException(msg); + })); + } + } + + private class IdToPartitionCacheLoader implements @NonNull CacheLoader { + @Nullable + @Override + public PartitionEntity load(@NonNull Integer theId) { + return myTxTemplate.execute(t -> myPartitionDao + .findById(theId) + .orElseThrow(() -> { + String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionConfigSvcImpl.class, "unknownPartitionId", theId); + return new IllegalArgumentException(msg); + })); + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.java new file mode 100644 index 00000000000..1151ed550ee --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.java @@ -0,0 +1,113 @@ +package ca.uhn.fhir.jpa.partition; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.entity.PartitionEntity; +import ca.uhn.fhir.jpa.model.util.ProviderConstants; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.util.ParametersUtil; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Optional; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +public class PartitionManagementProvider { + + @Autowired + private FhirContext myCtx; + @Autowired + private IPartitionConfigSvc myPartitionConfigSvc; + + /** + * Add Partition: + * + * $partition-management-add-partition + * + */ + @Operation(name = ProviderConstants.PARTITION_MANAGEMENT_ADD_PARTITION) + public IBaseParameters addPartition( + @ResourceParam IBaseParameters theRequest, + @OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, min = 1, max = 1, typeName = "integer") IPrimitiveType thePartitionId, + @OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, min = 1, max = 1, typeName = "code") IPrimitiveType thePartitionName, + @OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, min = 0, max = 1, typeName = "string") IPrimitiveType thePartitionDescription + ) { + + PartitionEntity input = parseInput(thePartitionId, thePartitionName, thePartitionDescription); + PartitionEntity output = myPartitionConfigSvc.createPartition(input); + IBaseParameters retVal = prepareOutput(output); + + return retVal; + } + + /** + * Add Partition: + * + * $partition-management-update-partition + * + */ + @Operation(name = ProviderConstants.PARTITION_MANAGEMENT_ADD_PARTITION) + public IBaseParameters updatePartition( + @ResourceParam IBaseParameters theRequest, + @OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, min = 1, max = 1, typeName = "integer") IPrimitiveType thePartitionId, + @OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, min = 1, max = 1, typeName = "code") IPrimitiveType thePartitionName, + @OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, min = 0, max = 1, typeName = "string") IPrimitiveType thePartitionDescription + ) { + + PartitionEntity input = parseInput(thePartitionId, thePartitionName, thePartitionDescription); + PartitionEntity output = myPartitionConfigSvc.updatePartition(input); + IBaseParameters retVal = prepareOutput(output); + + return retVal; + } + + /** + * Add Partition: + * + * $partition-management-delete-partition + * + */ + @Operation(name = ProviderConstants.PARTITION_MANAGEMENT_ADD_PARTITION) + public IBaseParameters updatePartition( + @ResourceParam IBaseParameters theRequest, + @OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, min = 1, max = 1, typeName = "integer") IPrimitiveType thePartitionId + ) { + + myPartitionConfigSvc.deletePartition(thePartitionId.getValue()); + + IBaseParameters retVal = ParametersUtil.newInstance(myCtx); + ParametersUtil.addParameterToParametersString(myCtx, retVal, "message", "Success"); + + return retVal; + } + + private IBaseParameters prepareOutput(PartitionEntity theOutput) { + IBaseParameters retVal = ParametersUtil.newInstance(myCtx); + ParametersUtil.addParameterToParametersInteger(myCtx, retVal, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, theOutput.getId()); + ParametersUtil.addParameterToParametersCode(myCtx, retVal, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, theOutput.getName()); + if (isNotBlank(theOutput.getDescription())) { + ParametersUtil.addParameterToParametersString(myCtx, retVal, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, theOutput.getDescription()); + } + return retVal; + } + + @NotNull + private PartitionEntity parseInput(@OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, min = 1, max = 1, typeName = "integer") IPrimitiveType thePartitionId, @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, min = 1, max = 1, typeName = "code") IPrimitiveType thePartitionName, @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, min = 0, max = 1, typeName = "string") IPrimitiveType thePartitionDescription) { + PartitionEntity input = new PartitionEntity(); + if (thePartitionId != null) { + input.setId(thePartitionId.getValue()); + } + if (thePartitionName != null) { + input.setName(thePartitionName.getValue()); + } + if (thePartitionDescription != null) { + input.setDescription(thePartitionDescription.getValue()); + } + return input; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/partition/RequestPartitionHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperService.java similarity index 82% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/partition/RequestPartitionHelperService.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperService.java index 222ef60567d..3ad134adade 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/partition/RequestPartitionHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperService.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.dao.partition; +package ca.uhn.fhir.jpa.partition; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.HookParams; @@ -8,7 +8,6 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.model.entity.PartitionId; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -29,6 +28,8 @@ public class RequestPartitionHelperService { @Autowired private IInterceptorBroadcaster myInterceptorBroadcaster; @Autowired + private IPartitionConfigSvc myPartitionConfigSvc; + @Autowired private FhirContext myFhirContext; public RequestPartitionHelperService() { @@ -66,7 +67,7 @@ public class RequestPartitionHelperService { .addIfMatchesType(ServletRequestDetails.class, theRequest); partitionId = (PartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_READ, params); - validatePartition(partitionId, theResourceType); + validatePartition(partitionId, theResourceType, theRequest); } return partitionId; @@ -88,18 +89,29 @@ public class RequestPartitionHelperService { partitionId = (PartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params); String resourceName = myFhirContext.getResourceDefinition(theResource).getName(); - validatePartition(partitionId, resourceName); + validatePartition(partitionId, resourceName, theRequest); } return partitionId; } - private void validatePartition(@Nullable PartitionId thePartitionId, @Nonnull String theResourceName) { + private void validatePartition(@Nullable PartitionId thePartitionId, @Nonnull String theResourceName, RequestDetails theRequestDetails) { if (thePartitionId != null && thePartitionId.getPartitionId() != null) { + + // Make sure we're not using one of the conformance resources in a non-default partition if (myPartitioningBlacklist.contains(theResourceName)) { String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperService.class, "blacklistedResourceTypeForPartitioning", theResourceName); throw new UnprocessableEntityException(msg); } + + // Make sure the partition exists + try { + myPartitionConfigSvc.getPartitionById(thePartitionId.getPartitionId()); + } catch (IllegalArgumentException e) { + String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperService.class, "unknownPartitionId", thePartitionId.getPartitionId()); + throw new InvalidRequestException(msg); + } + } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantSelectingInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantSelectingInterceptor.java new file mode 100644 index 00000000000..85b30772dc5 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestTenantSelectingInterceptor.java @@ -0,0 +1,49 @@ +package ca.uhn.fhir.jpa.partition; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.entity.PartitionEntity; +import ca.uhn.fhir.jpa.model.entity.PartitionId; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +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; + +public class RequestTenantSelectingInterceptor { + + @Autowired + private IPartitionConfigSvc myPartitionConfigSvc; + @Autowired + private FhirContext myFhirContext; + + @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE) + public PartitionId PartitionIdentifyCreate(IBaseResource theResource, ServletRequestDetails theRequestDetails) { + return extractPartitionIdFromRequest(theRequestDetails); + } + + @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ) + public PartitionId PartitionIdentifyRead(ServletRequestDetails theRequestDetails) { + return extractPartitionIdFromRequest(theRequestDetails); + } + + @NotNull + private PartitionId extractPartitionIdFromRequest(ServletRequestDetails theRequestDetails) { + String tenantId = theRequestDetails.getTenantId(); + + PartitionEntity partition; + try { + partition = myPartitionConfigSvc.getPartitionByName(tenantId); + } catch (IllegalArgumentException e) { + String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestTenantSelectingInterceptor.class, "unknownTenantName", tenantId); + throw new ResourceNotFoundException(msg); + } + + PartitionId retVal = new PartitionId(); + retVal.setPartitionId(partition.getId()); + return retVal; + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java index 3c4939e592a..0b88972d870 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java @@ -32,7 +32,7 @@ import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.dao.IResultIterator; import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; -import ca.uhn.fhir.jpa.dao.partition.RequestPartitionHelperService; +import ca.uhn.fhir.jpa.partition.RequestPartitionHelperService; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchInclude; import ca.uhn.fhir.jpa.entity.SearchTypeEnum; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index 3955361d5fd..09f638bc87f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -111,7 +111,7 @@ public class TestR4Config extends BaseJavaConfigR4 { SLF4JLogLevel level = SLF4JLogLevel.INFO; DataSource dataSource = ProxyDataSourceBuilder .create(retVal) -// .logQueryBySlf4j(level, "SQL") + .logQueryBySlf4j(level, "SQL") .logSlowQueryBySlf4j(10, TimeUnit.SECONDS) // .countQuery(new ThreadQueryCountHolder()) .beforeQuery(new BlockLargeNumbersOfParamsListener()) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index ea185414500..67d79984b8b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.executor.InterceptorService; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.partition.IPartitionConfigSvc; import ca.uhn.fhir.test.BaseTest; import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc; import ca.uhn.fhir.jpa.entity.TermConcept; @@ -108,6 +109,8 @@ public abstract class BaseJpaTest extends BaseTest { protected ISearchResultCacheSvc mySearchResultCacheSvc; @Autowired protected ISearchCacheSvc mySearchCacheSvc; + @Autowired + protected IPartitionConfigSvc myPartitionConfigSvc; @After public void afterPerformCleanup() { @@ -115,6 +118,9 @@ public abstract class BaseJpaTest extends BaseTest { if (myCaptureQueriesListener != null) { myCaptureQueriesListener.clear(); } + if (myPartitionConfigSvc != null) { + myPartitionConfigSvc.clearCaches(); + } } @After @@ -144,6 +150,13 @@ public abstract class BaseJpaTest extends BaseTest { } } + @Before + public void beforeInitPartitions() { + if (myPartitionConfigSvc != null) { + myPartitionConfigSvc.start(); + } + } + @Before public void beforeInitMocks() { myRequestOperationCallback = new InterceptorService(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index 4b6177654b1..6d976d95ad4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.interceptor.PerformanceTracingLoggingInterceptor; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.partition.IPartitionConfigSvc; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; @@ -100,6 +101,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { private static IValidationSupport ourJpaValidationSupportChainR4; private static IFhirResourceDaoValueSet ourValueSetDao; + @Autowired + protected IPartitionConfigSvc myPartitionConfigSvc; @Autowired protected ITermReadSvc myHapiTerminologySvc; @Autowired @@ -440,8 +443,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.ENABLED); } - @Before - public void beforePurgeDatabase() { + @After + public void afterPurgeDatabase() { purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc); } 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 2728ba7866f..949789d60e3 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 @@ -4,7 +4,9 @@ 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.api.config.DaoConfig; +import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.partition.IPartitionConfigSvc; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; @@ -13,6 +15,7 @@ import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParamModifier; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; @@ -33,6 +36,7 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.ServletException; import java.time.LocalDate; @@ -61,6 +65,8 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { private LocalDate myPartitionDate; private int myPartitionId; private boolean myHaveDroppedForcedIdUniqueConstraint; + @Autowired + private IPartitionConfigSvc myPartitionConfigSvc; @After public void after() { @@ -93,6 +99,10 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { myPartitionInterceptor = new MyInterceptor(); myInterceptorRegistry.registerInterceptor(myPartitionInterceptor); + + myPartitionConfigSvc.createPartition(new PartitionEntity().setId(1).setName("PART-1")); + myPartitionConfigSvc.createPartition(new PartitionEntity().setId(2).setName("PART-2")); + myPartitionConfigSvc.createPartition(new PartitionEntity().setId(3).setName("PART-3")); } @Test @@ -155,6 +165,22 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { } } + @Test + public void testCreate_UnknownPartition() { + addCreatePartition(99, null); + + Patient p = new Patient(); + p.addIdentifier().setSystem("system").setValue("value"); + p.setBirthDate(new Date()); + try { + myPatientDao.create(p); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Unknown partition ID: 99", e.getMessage()); + } + + } + @Test public void testCreate_ServerId_NoPartition() { addCreateNoPartition(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionConfigSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionConfigSvcImplTest.java new file mode 100644 index 00000000000..0c0cc3587aa --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionConfigSvcImplTest.java @@ -0,0 +1,173 @@ +package ca.uhn.fhir.jpa.partition; + +import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; +import ca.uhn.fhir.jpa.entity.PartitionEntity; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class PartitionConfigSvcImplTest extends BaseJpaR4Test { + + @Test + public void testCreateAndFetchPartition() { + + PartitionEntity partition = new PartitionEntity(); + partition.setId(123); + partition.setName("NAME123"); + partition.setDescription("A description"); + myPartitionConfigSvc.createPartition(partition); + + partition = myPartitionConfigSvc.getPartitionById(123); + assertEquals("NAME123", partition.getName()); + + partition = myPartitionConfigSvc.getPartitionByName("NAME123"); + assertEquals("NAME123", partition.getName()); + } + + @Test + public void testDeletePartition() { + + PartitionEntity partition = new PartitionEntity(); + partition.setId(123); + partition.setName("NAME123"); + partition.setDescription("A description"); + myPartitionConfigSvc.createPartition(partition); + + partition = myPartitionConfigSvc.getPartitionById(123); + assertEquals("NAME123", partition.getName()); + + myPartitionConfigSvc.deletePartition(123); + + try { + myPartitionConfigSvc.getPartitionById(123); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("No partition exists with ID 123", e.getMessage()); + } + + } + + @Test + public void testDeletePartition_TryToDeleteDefault() { + + try { + myPartitionConfigSvc.deletePartition(0); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Can not delete default partition", e.getMessage()); + } + + } + + @Test + public void testUpdatePartition_TryToUseExistingName() { + + PartitionEntity partition = new PartitionEntity(); + partition.setId(123); + partition.setName("NAME123"); + partition.setDescription("A description"); + myPartitionConfigSvc.createPartition(partition); + + partition = new PartitionEntity(); + partition.setId(111); + partition.setName("NAME111"); + partition.setDescription("A description"); + myPartitionConfigSvc.createPartition(partition); + + partition = new PartitionEntity(); + partition.setId(111); + partition.setName("NAME123"); + partition.setDescription("A description"); + try { + myPartitionConfigSvc.updatePartition(partition); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Partition name \"NAME123\" is already defined", e.getMessage()); + } + } + + @Test + public void testUpdatePartition_TryToRenameDefault() { + PartitionEntity partition = new PartitionEntity(); + partition.setId(0); + partition.setName("NAME123"); + partition.setDescription("A description"); + try { + myPartitionConfigSvc.updatePartition(partition); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Can not rename default partition", e.getMessage()); + } + } + + @Test + public void testUpdatePartition() { + + PartitionEntity partition = new PartitionEntity(); + partition.setId(123); + partition.setName("NAME123"); + partition.setDescription("A description"); + myPartitionConfigSvc.createPartition(partition); + + partition = myPartitionConfigSvc.getPartitionById(123); + assertEquals("NAME123", partition.getName()); + + partition = new PartitionEntity(); + partition.setId(123); + partition.setName("NAME-NEW"); + partition.setDescription("A description"); + myPartitionConfigSvc.updatePartition(partition); + + partition = myPartitionConfigSvc.getPartitionById(123); + assertEquals("NAME-NEW", partition.getName()); + } + + @Test + public void testCreatePartition_InvalidName() { + + PartitionEntity partition = new PartitionEntity(); + partition.setId(123); + partition.setName("NAME 123"); + partition.setDescription("A description"); + try { + myPartitionConfigSvc.createPartition(partition); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Partition name \"NAME 123\" is not valid", e.getMessage()); + } + + } + + @Test + public void testCreatePartition_0Blocked() { + PartitionEntity partition = new PartitionEntity(); + partition.setId(0); + partition.setName("NAME123"); + partition.setDescription("A description"); + try { + myPartitionConfigSvc.createPartition(partition); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Can not create a partition with ID 0 (this is a reserved value)", e.getMessage()); + } + + } + + @Test + public void testUpdatePartition_UnknownPartitionBlocked() { + PartitionEntity partition = new PartitionEntity(); + partition.setId(123); + partition.setName("NAME123"); + partition.setDescription("A description"); + try { + myPartitionConfigSvc.updatePartition(partition); + fail(); + } catch (InvalidRequestException e) { + assertEquals("No partition exists with ID 123", e.getMessage()); + } + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionManagementProviderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionManagementProviderTest.java new file mode 100644 index 00000000000..22620386365 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionManagementProviderTest.java @@ -0,0 +1,99 @@ +package ca.uhn.fhir.jpa.partition; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.entity.PartitionEntity; +import ca.uhn.fhir.jpa.model.util.ProviderConstants; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.test.utilities.server.RestfulServerRule; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.StringType; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = PartitionManagementProviderTest.MyConfig.class) +public class PartitionManagementProviderTest { + + private static final Logger ourLog = LoggerFactory.getLogger(PartitionManagementProviderTest.class); + private static FhirContext ourCtx = FhirContext.forR4(); + @ClassRule + public static RestfulServerRule ourServerRule = new RestfulServerRule(ourCtx); + @MockBean + private IPartitionConfigSvc myPartitionConfigSvc; + @Autowired + private PartitionManagementProvider myPartitionManagementProvider; + private IGenericClient myClient; + + @Before + public void before() { + ourServerRule.getRestfulServer().registerProvider(myPartitionManagementProvider); + myClient = ourServerRule.getFhirClient(); + } + + @After + public void after() { + ourServerRule.getRestfulServer().unregisterProvider(myPartitionManagementProvider); + } + + @Test + public void testAddPartition() { + when(myPartitionConfigSvc.createPartition(any())).thenAnswer(t -> t.getArgument(0, PartitionEntity.class)); + + Parameters input = new Parameters(); + input.addParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(123)); + input.addParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, new CodeType("PARTITION-123")); + input.addParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, new CodeType("a description")); + + Parameters response = myClient + .operation() + .onServer() + .named(ProviderConstants.PARTITION_MANAGEMENT_ADD_PARTITION) + .withParameters(input) + .encodedXml() + .execute(); + + ourLog.info("Response:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(response)); + verify(myPartitionConfigSvc, times(1)).createPartition(any()); + verifyNoMoreInteractions(myPartitionConfigSvc); + + assertEquals(123, ((IntegerType) response.getParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID)).getValue().intValue()); + assertEquals("PARTITION-123", ((StringType) response.getParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME)).getValue()); + assertEquals("a description", ((StringType) response.getParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC)).getValue()); + } + + @Configuration + public static class MyConfig { + + @Bean + public PartitionManagementProvider partitionManagementProvider() { + return new PartitionManagementProvider(); + } + + @Bean + public FhirContext fhirContext() { + return ourCtx; + } + + } + +} diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 814f36c553c..bbd5f177132 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -69,9 +69,14 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { version.onTable("HFJ_RES_VER").addForeignKey("20200218.3", "FK_RESOURCE_HISTORY_RESOURCE").toColumn("RES_ID").references("HFJ_RESOURCE", "RES_ID"); version.onTable("HFJ_RES_VER").modifyColumn("20200220.1", "RES_ID").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); - // Add mlutiitenancy + // Add multiitenancy version.onTable("HFJ_RESOURCE").dropIndex("20200327.1", "IDX_RES_PROFILE"); version.onTable("HFJ_RESOURCE").dropColumn("20200327.2", "RES_PROFILE"); + + Builder.BuilderAddTableByColumns partition = version.addTableByColumns("20200410.1", "HFJ_PARTITION", "PART_ID"); + partition.addColumn("PART_ID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.INT); + partition.addColumn("PART_NAME").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + partition.addColumn("PART_DESC").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); } protected void init420() { // 20191015 - 20200217 diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index a1cce679076..8bd89b237f5 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -134,6 +134,10 @@ spring-test test + + org.hibernate.validator + hibernate-validator + diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/ProviderConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/ProviderConstants.java index c1ecab6fa9a..f26a6926262 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/ProviderConstants.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/ProviderConstants.java @@ -23,4 +23,18 @@ package ca.uhn.fhir.jpa.model.util; public class ProviderConstants { public static final String SUBSCRIPTION_TRIGGERING_PARAM_RESOURCE_ID = "resourceId"; public static final String SUBSCRIPTION_TRIGGERING_PARAM_SEARCH_URL = "searchUrl"; + + /** + * Operation name: add partition + */ + public static final String PARTITION_MANAGEMENT_ADD_PARTITION = "partition-management-add-partition"; + + /** + * Operation name: update partition + */ + public static final String PARTITION_MANAGEMENT_MODIFY_PARTITION = "partition-management-update-partition"; + + public static final String PARTITION_MANAGEMENT_PARTITION_ID = "partitionId"; + public static final String PARTITION_MANAGEMENT_PARTITION_NAME = "partitionName"; + public static final String PARTITION_MANAGEMENT_PARTITION_DESC = "description"; } diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 4fbfa852fae..b9c013dda88 100755 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -100,6 +100,22 @@ org.springframework.retry spring-retry + + org.springframework + spring-beans + + + org.springframework + spring-context + + + org.hibernate + hibernate-search-engine + + + org.jscience + jscience + org.quartz-scheduler diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java index 63a86594f98..55c5b858b32 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java @@ -44,7 +44,6 @@ import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/ParametersUtilR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/ParametersUtilR4Test.java index d72f946e6c4..bcb4cceb339 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/ParametersUtilR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/ParametersUtilR4Test.java @@ -4,13 +4,16 @@ import ca.uhn.fhir.context.FhirContext; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.StringType; import org.junit.Test; import java.util.List; +import java.util.Optional; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class ParametersUtilR4Test { @@ -47,4 +50,16 @@ public class ParametersUtilR4Test { MatcherAssert.assertThat(values, Matchers.contains("VALUE1", "VALUE2")); } + @Test + public void testGetValueAsInteger(){ + Parameters p = new Parameters(); + p.addParameter() + .setName("foo") + .setValue(new IntegerType(123)); + + Optional value = ParametersUtil.getNamedParameterValueAsInteger(FhirContext.forR4(), p, "foo"); + assertTrue(value.isPresent()); + assertEquals(123, value.get().intValue()); + } + } diff --git a/pom.xml b/pom.xml index 88563003e56..12b2d8bb19c 100644 --- a/pom.xml +++ b/pom.xml @@ -1318,6 +1318,11 @@ hibernate-search-elasticsearch ${hibernate_search_version} + + org.hibernate + hibernate-search-engine + ${hibernate_version} + org.javassist javassist