Work on multitenancy

This commit is contained in:
jamesagnew 2020-04-10 20:01:59 -04:00
parent 8d8c657ce2
commit e55ccf88fc
28 changed files with 940 additions and 19 deletions

View File

@ -30,6 +30,9 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; 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 * Utilities for dealing with parameters resources in a version indepenedent way
@ -37,12 +40,26 @@ import java.util.Optional;
public class ParametersUtil { public class ParametersUtil {
public static List<String> getNamedParameterValuesAsString(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) { public static List<String> getNamedParameterValuesAsString(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) {
Function<IPrimitiveType<?>, String> mapper = t -> defaultIfBlank(t.getValueAsString(), null);
return extractNamedParameters(theCtx, theParameters, theParameterName, mapper);
}
public static List<Integer> getNamedParameterValuesAsInteger(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) {
Function<IPrimitiveType<?>, Integer> mapper = t -> (Integer)t.getValue();
return extractNamedParameters(theCtx, theParameters, theParameterName, mapper);
}
public static Optional<Integer> getNamedParameterValueAsInteger(FhirContext theCtx, IBaseParameters theParameters, String theParameterName) {
return getNamedParameterValuesAsInteger(theCtx, theParameters, theParameterName).stream().findFirst();
}
private static <T> List<T> extractNamedParameters(FhirContext theCtx, IBaseParameters theParameters, String theParameterName, Function<IPrimitiveType<?>, T> theMapper) {
Validate.notNull(theParameters, "theParameters must not be null"); Validate.notNull(theParameters, "theParameters must not be null");
RuntimeResourceDefinition resDef = theCtx.getResourceDefinition(theParameters.getClass()); RuntimeResourceDefinition resDef = theCtx.getResourceDefinition(theParameters.getClass());
BaseRuntimeChildDefinition parameterChild = resDef.getChildByName("parameter"); BaseRuntimeChildDefinition parameterChild = resDef.getChildByName("parameter");
List<IBase> parameterReps = parameterChild.getAccessor().getValues(theParameters); List<IBase> parameterReps = parameterChild.getAccessor().getValues(theParameters);
List<String> retVal = new ArrayList<>(); List<T> retVal = new ArrayList<>();
for (IBase nextParameter : parameterReps) { for (IBase nextParameter : parameterReps) {
BaseRuntimeElementCompositeDefinition<?> nextParameterDef = (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(nextParameter.getClass()); BaseRuntimeElementCompositeDefinition<?> nextParameterDef = (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(nextParameter.getClass());
@ -62,12 +79,12 @@ public class ParametersUtil {
valueValues valueValues
.stream() .stream()
.filter(t -> t instanceof IPrimitiveType<?>) .filter(t -> t instanceof IPrimitiveType<?>)
.map(t -> ((IPrimitiveType<?>) t).getValueAsString()) .map(t->((IPrimitiveType<?>) t))
.filter(StringUtils::isNotBlank) .map(theMapper)
.filter(t -> t != null)
.forEach(retVal::add); .forEach(retVal::add);
} }
return retVal; return retVal;
} }

View File

@ -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.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.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.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.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}

View File

@ -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 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) * [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 # Request Pre-Processing: Override Meta.source

View File

@ -11,7 +11,10 @@ import ca.uhn.fhir.jpa.bulk.BulkDataExportProvider;
import ca.uhn.fhir.jpa.bulk.BulkDataExportSvcImpl; import ca.uhn.fhir.jpa.bulk.BulkDataExportSvcImpl;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc; import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.dao.ISearchBuilder; 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.entity.Search;
import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
@ -225,6 +228,18 @@ public abstract class BaseConfig {
return new JpaConsentContextServices(); return new JpaConsentContextServices();
} }
@Bean
@Lazy
public IPartitionConfigSvc partitionConfigSvc() {
return new PartitionConfigSvcImpl();
}
@Bean
@Lazy
public PartitionManagementProvider partitionManagementProvider() {
return new PartitionManagementProvider();
}
@Bean @Bean
@Lazy @Lazy
public TerminologyUploaderProvider terminologyUploaderProvider() { public TerminologyUploaderProvider terminologyUploaderProvider() {

View File

@ -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.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; 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.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource; import ca.uhn.fhir.jpa.model.entity.BaseHasResource;

View File

@ -23,7 +23,7 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService; 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.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.PartitionId; import ca.uhn.fhir.jpa.model.entity.PartitionId;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;

View File

@ -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<PartitionEntity, Integer> {
@Query("SELECT p FROM PartitionEntity p WHERE p.myName = :name")
Optional<PartitionEntity> findForName(@Param("name") String theName);
}

View File

@ -121,6 +121,7 @@ public class ExpungeEverythingService {
counter.addAndGet(expungeEverythingByType(ResourceHistoryProvenanceEntity.class)); counter.addAndGet(expungeEverythingByType(ResourceHistoryProvenanceEntity.class));
counter.addAndGet(expungeEverythingByType(ResourceHistoryTable.class)); counter.addAndGet(expungeEverythingByType(ResourceHistoryTable.class));
counter.addAndGet(expungeEverythingByType(ResourceTable.class)); counter.addAndGet(expungeEverythingByType(ResourceTable.class));
counter.addAndGet(expungeEverythingByType(PartitionEntity.class));
myTxTemplate.execute(t -> { myTxTemplate.execute(t -> {
counter.addAndGet(doExpungeEverythingQuery("DELETE from " + org.hibernate.search.jpa.Search.class.getSimpleName() + " d")); counter.addAndGet(doExpungeEverythingQuery("DELETE from " + org.hibernate.search.jpa.Search.class.getSimpleName() + " d"));
return null; return null;

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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<String, PartitionEntity> myNameToPartitionCache;
private LoadingCache<Integer, PartitionEntity> 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<PartitionEntity> 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<PartitionEntity> 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<String, PartitionEntity> {
@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<Integer, PartitionEntity> {
@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);
}));
}
}
}

View File

@ -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:
* <code>
* $partition-management-add-partition
* </code>
*/
@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<Integer> thePartitionId,
@OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, min = 1, max = 1, typeName = "code") IPrimitiveType<String> thePartitionName,
@OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, min = 0, max = 1, typeName = "string") IPrimitiveType<String> thePartitionDescription
) {
PartitionEntity input = parseInput(thePartitionId, thePartitionName, thePartitionDescription);
PartitionEntity output = myPartitionConfigSvc.createPartition(input);
IBaseParameters retVal = prepareOutput(output);
return retVal;
}
/**
* Add Partition:
* <code>
* $partition-management-update-partition
* </code>
*/
@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<Integer> thePartitionId,
@OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, min = 1, max = 1, typeName = "code") IPrimitiveType<String> thePartitionName,
@OperationParam(name=ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, min = 0, max = 1, typeName = "string") IPrimitiveType<String> thePartitionDescription
) {
PartitionEntity input = parseInput(thePartitionId, thePartitionName, thePartitionDescription);
PartitionEntity output = myPartitionConfigSvc.updatePartition(input);
IBaseParameters retVal = prepareOutput(output);
return retVal;
}
/**
* Add Partition:
* <code>
* $partition-management-delete-partition
* </code>
*/
@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<Integer> 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<Integer> thePartitionId, @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, min = 1, max = 1, typeName = "code") IPrimitiveType<String> thePartitionName, @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, min = 0, max = 1, typeName = "string") IPrimitiveType<String> 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;
}
}

View File

@ -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.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams; 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.jpa.model.entity.PartitionId;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 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.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
@ -29,6 +28,8 @@ public class RequestPartitionHelperService {
@Autowired @Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster; private IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired @Autowired
private IPartitionConfigSvc myPartitionConfigSvc;
@Autowired
private FhirContext myFhirContext; private FhirContext myFhirContext;
public RequestPartitionHelperService() { public RequestPartitionHelperService() {
@ -66,7 +67,7 @@ public class RequestPartitionHelperService {
.addIfMatchesType(ServletRequestDetails.class, theRequest); .addIfMatchesType(ServletRequestDetails.class, theRequest);
partitionId = (PartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_READ, params); partitionId = (PartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_READ, params);
validatePartition(partitionId, theResourceType); validatePartition(partitionId, theResourceType, theRequest);
} }
return partitionId; return partitionId;
@ -88,18 +89,29 @@ public class RequestPartitionHelperService {
partitionId = (PartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params); partitionId = (PartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params);
String resourceName = myFhirContext.getResourceDefinition(theResource).getName(); String resourceName = myFhirContext.getResourceDefinition(theResource).getName();
validatePartition(partitionId, resourceName); validatePartition(partitionId, resourceName, theRequest);
} }
return partitionId; 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) { 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)) { if (myPartitioningBlacklist.contains(theResourceName)) {
String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperService.class, "blacklistedResourceTypeForPartitioning", theResourceName); String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperService.class, "blacklistedResourceTypeForPartitioning", theResourceName);
throw new UnprocessableEntityException(msg); 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);
}
} }
} }
} }

View File

@ -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;
}
}

View File

@ -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.IResultIterator;
import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; 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.Search;
import ca.uhn.fhir.jpa.entity.SearchInclude; import ca.uhn.fhir.jpa.entity.SearchInclude;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.jpa.entity.SearchTypeEnum;

View File

@ -111,7 +111,7 @@ public class TestR4Config extends BaseJavaConfigR4 {
SLF4JLogLevel level = SLF4JLogLevel.INFO; SLF4JLogLevel level = SLF4JLogLevel.INFO;
DataSource dataSource = ProxyDataSourceBuilder DataSource dataSource = ProxyDataSourceBuilder
.create(retVal) .create(retVal)
// .logQueryBySlf4j(level, "SQL") .logQueryBySlf4j(level, "SQL")
.logSlowQueryBySlf4j(10, TimeUnit.SECONDS) .logSlowQueryBySlf4j(10, TimeUnit.SECONDS)
// .countQuery(new ThreadQueryCountHolder()) // .countQuery(new ThreadQueryCountHolder())
.beforeQuery(new BlockLargeNumbersOfParamsListener()) .beforeQuery(new BlockLargeNumbersOfParamsListener())

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.executor.InterceptorService; import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; 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.test.BaseTest;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc; import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConcept;
@ -108,6 +109,8 @@ public abstract class BaseJpaTest extends BaseTest {
protected ISearchResultCacheSvc mySearchResultCacheSvc; protected ISearchResultCacheSvc mySearchResultCacheSvc;
@Autowired @Autowired
protected ISearchCacheSvc mySearchCacheSvc; protected ISearchCacheSvc mySearchCacheSvc;
@Autowired
protected IPartitionConfigSvc myPartitionConfigSvc;
@After @After
public void afterPerformCleanup() { public void afterPerformCleanup() {
@ -115,6 +118,9 @@ public abstract class BaseJpaTest extends BaseTest {
if (myCaptureQueriesListener != null) { if (myCaptureQueriesListener != null) {
myCaptureQueriesListener.clear(); myCaptureQueriesListener.clear();
} }
if (myPartitionConfigSvc != null) {
myPartitionConfigSvc.clearCaches();
}
} }
@After @After
@ -144,6 +150,13 @@ public abstract class BaseJpaTest extends BaseTest {
} }
} }
@Before
public void beforeInitPartitions() {
if (myPartitionConfigSvc != null) {
myPartitionConfigSvc.start();
}
}
@Before @Before
public void beforeInitMocks() { public void beforeInitMocks() {
myRequestOperationCallback = new InterceptorService(); myRequestOperationCallback = new InterceptorService();

View File

@ -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.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; 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.provider.r4.JpaSystemProviderR4;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
@ -100,6 +101,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
private static IValidationSupport ourJpaValidationSupportChainR4; private static IValidationSupport ourJpaValidationSupportChainR4;
private static IFhirResourceDaoValueSet<ValueSet, Coding, CodeableConcept> ourValueSetDao; private static IFhirResourceDaoValueSet<ValueSet, Coding, CodeableConcept> ourValueSetDao;
@Autowired
protected IPartitionConfigSvc myPartitionConfigSvc;
@Autowired @Autowired
protected ITermReadSvc myHapiTerminologySvc; protected ITermReadSvc myHapiTerminologySvc;
@Autowired @Autowired
@ -440,8 +443,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.ENABLED); myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.ENABLED);
} }
@Before @After
public void beforePurgeDatabase() { public void afterPurgeDatabase() {
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc); purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc);
} }

View File

@ -4,7 +4,9 @@ import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor; import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.api.config.DaoConfig; 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.model.entity.*;
import ca.uhn.fhir.jpa.partition.IPartitionConfigSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.Constants; 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.StringParam;
import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.TokenParamModifier; 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.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
@ -33,6 +36,7 @@ import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import java.time.LocalDate; import java.time.LocalDate;
@ -61,6 +65,8 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
private LocalDate myPartitionDate; private LocalDate myPartitionDate;
private int myPartitionId; private int myPartitionId;
private boolean myHaveDroppedForcedIdUniqueConstraint; private boolean myHaveDroppedForcedIdUniqueConstraint;
@Autowired
private IPartitionConfigSvc myPartitionConfigSvc;
@After @After
public void after() { public void after() {
@ -93,6 +99,10 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
myPartitionInterceptor = new MyInterceptor(); myPartitionInterceptor = new MyInterceptor();
myInterceptorRegistry.registerInterceptor(myPartitionInterceptor); 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 @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 @Test
public void testCreate_ServerId_NoPartition() { public void testCreate_ServerId_NoPartition() {
addCreateNoPartition(); addCreateNoPartition();

View File

@ -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());
}
}
}

View File

@ -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;
}
}
}

View File

@ -69,9 +69,14 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
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").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); 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").dropIndex("20200327.1", "IDX_RES_PROFILE");
version.onTable("HFJ_RESOURCE").dropColumn("20200327.2", "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 protected void init420() { // 20191015 - 20200217

View File

@ -134,6 +134,10 @@
<artifactId>spring-test</artifactId> <artifactId>spring-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -23,4 +23,18 @@ package ca.uhn.fhir.jpa.model.util;
public class ProviderConstants { public class ProviderConstants {
public static final String SUBSCRIPTION_TRIGGERING_PARAM_RESOURCE_ID = "resourceId"; public static final String SUBSCRIPTION_TRIGGERING_PARAM_RESOURCE_ID = "resourceId";
public static final String SUBSCRIPTION_TRIGGERING_PARAM_SEARCH_URL = "searchUrl"; 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";
} }

View File

@ -100,6 +100,22 @@
<groupId>org.springframework.retry</groupId> <groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId> <artifactId>spring-retry</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-search-engine</artifactId>
</dependency>
<dependency>
<groupId>org.jscience</groupId>
<artifactId>jscience</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.quartz-scheduler</groupId> <groupId>org.quartz-scheduler</groupId>

View File

@ -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.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;

View File

@ -4,13 +4,16 @@ import ca.uhn.fhir.context.FhirContext;
import org.hamcrest.MatcherAssert; import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseParameters; 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.Parameters;
import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StringType;
import org.junit.Test; import org.junit.Test;
import java.util.List; import java.util.List;
import java.util.Optional;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class ParametersUtilR4Test { public class ParametersUtilR4Test {
@ -47,4 +50,16 @@ public class ParametersUtilR4Test {
MatcherAssert.assertThat(values, Matchers.contains("VALUE1", "VALUE2")); MatcherAssert.assertThat(values, Matchers.contains("VALUE1", "VALUE2"));
} }
@Test
public void testGetValueAsInteger(){
Parameters p = new Parameters();
p.addParameter()
.setName("foo")
.setValue(new IntegerType(123));
Optional<Integer> value = ParametersUtil.getNamedParameterValueAsInteger(FhirContext.forR4(), p, "foo");
assertTrue(value.isPresent());
assertEquals(123, value.get().intValue());
}
} }

View File

@ -1318,6 +1318,11 @@
<artifactId>hibernate-search-elasticsearch</artifactId> <artifactId>hibernate-search-elasticsearch</artifactId>
<version>${hibernate_search_version}</version> <version>${hibernate_search_version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-search-engine</artifactId>
<version>${hibernate_version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.javassist</groupId> <groupId>org.javassist</groupId>
<artifactId>javassist</artifactId> <artifactId>javassist</artifactId>