Work on multitenancy

This commit is contained in:
jamesagnew 2020-02-08 19:59:37 -05:00
parent b2d2346228
commit f7ec41ffc5
19 changed files with 216 additions and 91 deletions

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.interceptor.api;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
public interface IInterceptorService extends IInterceptorBroadcaster {
@ -90,4 +91,8 @@ public interface IInterceptorService extends IInterceptorBroadcaster {
void registerInterceptors(@Nullable Collection<?> theInterceptors);
/**
* Unregisters all interceptors that are indicated by the given callback function returning <code>true</code>
*/
void unregisterInterceptorsIf(Function<Object, Boolean> theShouldUnregisterFunction);
}

View File

@ -1321,6 +1321,43 @@ public enum Pointcut {
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* <b>Storage Hook:</b>
* Invoked before an <code>$expunge</code> operation on all data (expungeEverything) is called.
* <p>
* Hooks will be passed a reference to a counter containing the current number of records that have been deleted.
* If the hook deletes any records, the hook is expected to increment this counter by the number of records deleted.
* </p>
* Hooks may accept the following parameters:
* <ul>
* org.hl7.fhir.instance.model.api.IBaseResource - The resource that will be created and needs a tenant ID assigned.
* <li>
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred.
* </li>
* <li>
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* </ul>
* <p>
* Hooks should return an instance of <code>ca.uhn.fhir.jpa.model.entity.TenantId</code> or <code>null</code>.
* </p>
*/
STORAGE_TENANT_IDENTIFY_CREATE (
// Return type
"ca.uhn.fhir.jpa.model.entity.TenantId",
// Params
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
/**
* <b>Performance Tracing Hook:</b>
* This hook is invoked when any informational messages generated by the

View File

@ -41,6 +41,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
public class InterceptorService implements IInterceptorService, IInterceptorBroadcaster {
@ -145,6 +146,22 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
}
}
@Override
public void unregisterInterceptorsIf(Function<Object, Boolean> theShouldUnregisterFunction) {
unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers);
unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers);
}
private void unregisterInterceptorsIf(Function<Object, Boolean> theShouldUnregisterFunction, ListMultimap<Pointcut, BaseInvoker> theGlobalInvokers) {
for (Iterator<Map.Entry<Pointcut, BaseInvoker>> iter = theGlobalInvokers.entries().iterator(); iter.hasNext(); ) {
Map.Entry<Pointcut, BaseInvoker> next = iter.next();
Object nextInterceptor = next.getValue().getInterceptor();
if (theShouldUnregisterFunction.apply(nextInterceptor)) {
iter.remove();
}
}
}
@Override
public boolean registerThreadLocalInterceptor(Object theInterceptor) {
if (!myThreadlocalInvokersEnabled) {

View File

@ -4,7 +4,7 @@
title: "The version of a few dependencies have been bumped to the latest versions
(dependent HAPI modules listed in brackets):
<ul>
<li>Hibernate ORM (JPA): 5.4.6 -&gt; 5.4.10</li>
<li>Hibernate ORM (JPA): 5.4.6.Final -&gt; 5.4.11.Final</li>
</ul>"
- item:
type: change

View File

@ -390,6 +390,19 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
ResourceTable entity = new ResourceTable();
entity.setResourceType(toResourceName(theResource));
if (myDaoConfig.isMultiTenancyEnabled()) {
// Interceptor call: STORAGE_TENANT_IDENTIFY_CREATE
HookParams params = new HookParams()
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
TenantId tenantId = (TenantId) doCallHooksAndReturnObject(theRequest, Pointcut.STORAGE_TENANT_IDENTIFY_CREATE, params);
if (tenantId != null) {
ourLog.debug("Resource has been assigned tenant ID: {}", tenantId);
entity.setTenantId(tenantId);
}
}
if (isNotBlank(theIfNoneExist)) {
Set<ResourcePersistentId> match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType, theRequest);
if (match.size() > 1) {
@ -432,7 +445,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails);
}
// Notify JPA interceptors
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theResource)
.add(RequestDetails.class, theRequest)

View File

@ -54,7 +54,6 @@ import java.util.Set;
import static ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.OO_SEVERITY_ERROR;
import static ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.OO_SEVERITY_INFO;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseStorageDao {
@ -161,6 +160,10 @@ public abstract class BaseStorageDao {
JpaInterceptorBroadcaster.doCallHooks(getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams);
}
protected Object doCallHooksAndReturnObject(RequestDetails theRequestDetails, Pointcut thePointcut, HookParams theParams) {
return JpaInterceptorBroadcaster.doCallHooksAndReturnObject(getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams);
}
protected abstract IInterceptorBroadcaster getInterceptorBroadcaster();
public IBaseOperationOutcome createErrorOperationOutcome(String theMessage, String theCode) {

View File

@ -183,6 +183,11 @@ public class DaoConfig {
*/
private boolean myPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets;
/**
* @since 4.3.0
*/
private boolean myMultiTenancyEnabled;
/**
* Constructor
*/
@ -1907,7 +1912,25 @@ public class DaoConfig {
setPreExpandValueSetsDefaultCount(Math.min(getPreExpandValueSetsDefaultCount(), getPreExpandValueSetsMaxCount()));
}
public enum StoreMetaSourceInformationEnum {
/**
* If enabled (default is <code>false</code>) the JPA server will support multitenant queries
*
* @since 4.3.0
*/
public void setMultiTenancyEnabled(boolean theMultiTenancyEnabled) {
myMultiTenancyEnabled = theMultiTenancyEnabled;
}
/**
* If enabled (default is <code>false</code>) the JPA server will support multitenant queries
*
* @since 4.3.0
*/
public boolean isMultiTenancyEnabled() {
return myMultiTenancyEnabled;
}
public enum StoreMetaSourceInformationEnum {
NONE(false, false),
SOURCE_URI(true, false),
REQUEST_ID(false, true),

View File

@ -70,6 +70,7 @@ public class DaoSearchParamSynchronizer {
theEntity.getParamsQuantity().remove(next);
}
for (T next : quantitiesToAdd) {
next.setTenantId(theEntity.getTenantId());
myEntityManager.merge(next);
}

View File

@ -1,84 +1,31 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
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.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTag;
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.model.entity.TenantId;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent;
import org.hl7.fhir.r4.model.Bundle.BundleEntryResponseComponent;
import org.hl7.fhir.r4.model.Bundle.BundleType;
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
import org.hl7.fhir.r4.model.Observation.ObservationStatus;
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.r4.model.Patient;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import javax.servlet.ServletException;
import java.time.LocalDate;
import java.time.Month;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class MultitenantR4Test extends BaseJpaR4SystemTest {
@ -86,10 +33,17 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest {
@After
public void after() {
myDaoConfig.setMultiTenancyEnabled(new DaoConfig().isMultiTenancyEnabled());
myInterceptorRegistry.unregisterInterceptorsIf(t -> t instanceof MyInterceptor);
}
@Override
@Before
public void beforeDisableResultReuse() {
public void before() throws ServletException {
super.before();
myDaoConfig.setMultiTenancyEnabled(true);
}
@ -100,28 +54,57 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest {
p.setBirthDate(new Date());
Long patientId = myPatientDao.create(p).getId().getIdPartAsLong();
runInTransaction(()->{
ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(() -> new IllegalArgumentException());
runInTransaction(() -> {
ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new);
assertNull(resourceTable.getTenantId());
});
}
@Test
public void testCreateResourceWithTenant() {
int expectId = 3;
LocalDate expectDate = LocalDate.of(2020, Month.JANUARY, 14);
myInterceptorRegistry.registerInterceptor(new MyInterceptor(new TenantId(expectId, expectDate)));
Patient p = new Patient();
p.setUserData(JpaConstants.USERDATA_TENANT_ID, 3);
p.setUserData(JpaConstants.USERDATA_TENANT_DATE, LocalDate.of(2020, Month.JANUARY, 14));
p.addName().setFamily("FAM");
p.addIdentifier().setSystem("system").setValue("value");
p.setBirthDate(new Date());
Long patientId = myPatientDao.create(p).getId().getIdPartAsLong();
runInTransaction(()->{
ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(() -> new IllegalArgumentException());
assertNull(resourceTable.getTenantId());
runInTransaction(() -> {
ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new);
assertEquals(expectId, resourceTable.getTenantId().getTenantId().intValue());
assertEquals(expectDate, resourceTable.getTenantId().getTenantDate());
List<ResourceIndexedSearchParamString> strings = myResourceIndexedSearchParamStringDao.findAll();
ourLog.info("\n * {}", strings.stream().map(ResourceIndexedSearchParamString::toString).collect(Collectors.joining("\n * ")));
assertEquals(10, strings.size());
assertEquals(expectId, strings.get(0).getTenantId().getTenantId().intValue());
assertEquals(expectDate, strings.get(0).getTenantId().getTenantDate());
});
}
@Interceptor
public static class MyInterceptor {
private final List<TenantId> myTenantIds;
public MyInterceptor(TenantId theTenantId) {
Validate.notNull(theTenantId);
myTenantIds = Collections.singletonList(theTenantId);
}
@Hook(Pointcut.STORAGE_TENANT_IDENTIFY_CREATE)
public TenantId tenantIdentifyCreate() {
TenantId retVal = myTenantIds.get(0);
ourLog.info("Returning tenant ID: {}", retVal);
return retVal;
}
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -20,10 +20,24 @@ package ca.uhn.fhir.jpa.model.entity;
* #L%
*/
import javax.persistence.Embedded;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
@MappedSuperclass
public abstract class BaseResourceIndex implements Serializable {
@Embedded
private TenantId myTenantId;
public TenantId getTenantId() {
return myTenantId;
}
public void setTenantId(TenantId theTenantId) {
myTenantId = theTenantId;
}
public abstract Long getId();
public abstract void setId(Long theId);

View File

@ -80,17 +80,6 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex {
@Temporal(TemporalType.TIMESTAMP)
private Date myUpdated;
@Embedded
private TenantId myTenantId;
public TenantId getTenantId() {
return myTenantId;
}
public void setTenantId(TenantId theTenantId) {
myTenantId = theTenantId;
}
/**
* Subclasses may override
*/

View File

@ -221,6 +221,13 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
@Transient
private transient ResourceHistoryTable myCurrentVersionEntity;
/**
* Constructor
*/
public ResourceTable() {
super();
}
@Override
public ResourceTag addTag(TagDefinition theTag) {
for (ResourceTag next : getTags()) {
@ -423,6 +430,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
return this;
}
@Override
public Collection<ResourceTag> getTags() {
if (myTags == null) {
myTags = new HashSet<>();
@ -551,6 +559,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
retVal.setFhirVersion(getFhirVersion());
retVal.setDeleted(getDeleted());
retVal.setForcedId(getForcedId());
retVal.setTenantId(getTenantId());
retVal.getTags().clear();

View File

@ -1,5 +1,8 @@
package ca.uhn.fhir.jpa.model.entity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.time.LocalDate;
@ -12,7 +15,22 @@ public class TenantId implements Cloneable {
@Column(name = "TENANT_DATE", nullable = true)
private LocalDate myTenantDate;
public Integer getTenantId() {
/**
* Constructor
*/
public TenantId() {
super();
}
/**
* Constructor
*/
public TenantId(int theTenantId, LocalDate theTenantDate) {
setTenantId(theTenantId);
setTenantDate(theTenantDate);
}
public Integer getTenantId() {
return myTenantId;
}
@ -36,4 +54,12 @@ public class TenantId implements Cloneable {
.setTenantId(getTenantId())
.setTenantDate(getTenantDate());
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("id", myTenantId)
.append("date", myTenantDate)
.toString();
}
}

View File

@ -24,9 +24,6 @@ import ca.uhn.fhir.rest.api.Constants;
public class JpaConstants {
public static final String USERDATA_TENANT_ID = JpaConstants.class.getName() + "_USERDATA_TENANT_ID";
public static final String USERDATA_TENANT_DATE = JpaConstants.class.getName() + "_USERDATA_TENANT_DATE";
/**
* Operation name for the $apply-codesystem-delta-add operation
*/

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.dialect.PostgreSQL94Dialect;
import org.hl7.fhir.dstu2.model.Subscription;
import org.springframework.beans.factory.annotation.Value;
@ -94,6 +95,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 {
retVal.setUsername(myDbUsername);
retVal.setPassword(myDbPassword);
retVal.setDefaultQueryTimeout(20);
retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE);
return retVal;
}

View File

@ -10,6 +10,7 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.dialect.PostgreSQL94Dialect;
import org.hl7.fhir.dstu2.model.Subscription;
import org.springframework.beans.factory.annotation.Autowire;
@ -101,6 +102,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
retVal.setUsername(myDbUsername);
retVal.setPassword(myDbPassword);
retVal.setDefaultQueryTimeout(20);
retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE);
return retVal;
}

View File

@ -10,6 +10,7 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.dialect.PostgreSQL94Dialect;
import org.hl7.fhir.dstu2.model.Subscription;
import org.springframework.beans.factory.annotation.Autowire;
@ -86,6 +87,7 @@ public class TestR4Config extends BaseJavaConfigR4 {
retVal.setUsername(myDbUsername);
retVal.setPassword(myDbPassword);
retVal.setDefaultQueryTimeout(20);
retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE);
return retVal;
}

View File

@ -10,6 +10,7 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.dialect.PostgreSQL94Dialect;
import org.hl7.fhir.dstu2.model.Subscription;
import org.springframework.beans.factory.annotation.Autowire;
@ -86,6 +87,7 @@ public class TestR5Config extends BaseJavaConfigR5 {
retVal.setUsername(myDbUsername);
retVal.setPassword(myDbPassword);
retVal.setDefaultQueryTimeout(20);
retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE);
return retVal;
}

View File

@ -645,7 +645,7 @@
<jsr305_version>3.0.2</jsr305_version>
<flyway_version>6.1.0</flyway_version>
<!--<hibernate_version>5.2.10.Final</hibernate_version>-->
<hibernate_version>5.4.10.Final</hibernate_version>
<hibernate_version>5.4.11.Final</hibernate_version>
<!-- Update lucene version when you update hibernate-search version -->
<hibernate_search_version>5.11.3.Final</hibernate_search_version>
<lucene_version>5.5.5</lucene_version>
@ -1326,7 +1326,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.9</version>
<version>42.2.10</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>