Address use case where packages include non-conformance resources. Update documentation as well.

This commit is contained in:
ianmarshall 2021-01-06 10:29:10 -05:00
parent d01c775b99
commit 659e83f451
7 changed files with 162 additions and 15 deletions

View File

@ -139,3 +139,5 @@ None of the limitations listed here are considered permanent. Over time the HAPI
* **Cross-partition History Operations are not supported**: It is not possible to perform a `_history` operation that spans all partitions (`_history` does work when applied to a single partition however).
* **Bulk Operations are not partition aware**: Bulk export operations will export data across all partitions.
* **Package Operations are not partition aware**: Package operations will only affect resources defined in the default partition.

View File

@ -326,7 +326,7 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac
private ResourceTable createResourceBinary(IBaseBinary theResourceBinary) {
if (myPartitionSettings.isPartitioningEnabled()) {
PackageBinaryRequestDetails myRequestDetails = new PackageBinaryRequestDetails();
PackageSystemRequestDetails myRequestDetails = new PackageSystemRequestDetails();
return (ResourceTable) getBinaryDao().create(theResourceBinary, myRequestDetails).getEntity();
} else {
return (ResourceTable) getBinaryDao().create(theResourceBinary).getEntity();
@ -637,7 +637,7 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac
private void deleteAndExpungeResourceBinary(IIdType theResourceBinaryId, ExpungeOptions theOptions) {
if (myPartitionSettings.isPartitioningEnabled()) {
PackageBinaryRequestDetails myRequestDetails = new PackageBinaryRequestDetails();
PackageSystemRequestDetails myRequestDetails = new PackageSystemRequestDetails();
getBinaryDao().delete(theResourceBinaryId, myRequestDetails).getEntity();
getBinaryDao().forceExpungeInExistingTransaction(theResourceBinaryId, theOptions, myRequestDetails);
} else {

View File

@ -31,6 +31,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
@ -50,12 +51,12 @@ import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.utilities.npm.IPackageCacheManager;
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 org.hl7.fhir.utilities.npm.BasePackageCacheManager;
import org.hl7.fhir.utilities.npm.NpmPackage;
import javax.annotation.PostConstruct;
@ -102,7 +103,8 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc {
private INpmPackageVersionDao myPackageVersionDao;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private PartitionSettings myPartitionSettings;
/**
* Constructor
*/
@ -316,19 +318,19 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc {
private void create(IBaseResource theResource, PackageInstallOutcomeJson theOutcome) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
SearchParameterMap map = createSearchParameterMapFor(theResource);
IBundleProvider searchResult = dao.search(map);
IBundleProvider searchResult = searchResource(dao, map);
if (validForUpload(theResource)) {
if (searchResult.isEmpty()) {
ourLog.info("Creating new resource matching {}", map.toNormalizedQueryString(myFhirContext));
theOutcome.incrementResourcesInstalled(myFhirContext.getResourceType(theResource));
dao.create(theResource);
createResource(dao, theResource);
} else {
ourLog.info("Updating existing resource matching {}", map.toNormalizedQueryString(myFhirContext));
theResource.setId(searchResult.getResources(0, 1).get(0).getIdElement().toUnqualifiedVersionless());
DaoMethodOutcome outcome = dao.update(theResource);
DaoMethodOutcome outcome = updateResource(dao, theResource);
if (!outcome.isNop()) {
theOutcome.incrementResourcesInstalled(myFhirContext.getResourceType(theResource));
}
@ -337,6 +339,33 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc {
}
}
private IBundleProvider searchResource(IFhirResourceDao theDao, SearchParameterMap theMap) {
if (myPartitionSettings.isPartitioningEnabled()) {
PackageSystemRequestDetails myRequestDetails = new PackageSystemRequestDetails();
return theDao.search(theMap, myRequestDetails);
} else {
return theDao.search(theMap);
}
}
private void createResource(IFhirResourceDao theDao, IBaseResource theResource) {
if (myPartitionSettings.isPartitioningEnabled()) {
PackageSystemRequestDetails myRequestDetails = new PackageSystemRequestDetails();
theDao.create(theResource, myRequestDetails);
} else {
theDao.create(theResource);
}
}
private DaoMethodOutcome updateResource(IFhirResourceDao theDao, IBaseResource theResource) {
if (myPartitionSettings.isPartitioningEnabled()) {
PackageSystemRequestDetails myRequestDetails = new PackageSystemRequestDetails();
return theDao.update(theResource, myRequestDetails);
} else {
return theDao.update(theResource);
}
}
boolean validForUpload(IBaseResource theResource) {
String resourceType = myFhirContext.getResourceType(theResource);
if ("SearchParameter".equals(resourceType)) {

View File

@ -5,12 +5,12 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
public class PackageBinaryRequestDetails extends SystemRequestDetails {
/**
* Constructor
*
* A RequestDetails implementation to be used when processing resources defined in a package to ensure
* that they are always queried or updated in the DEFAULT partition if partitioning is enabled.
*/
public PackageBinaryRequestDetails() {
public class PackageSystemRequestDetails extends SystemRequestDetails {
public PackageSystemRequestDetails() {
super(new MyInterceptorBroadcaster());
}

View File

@ -28,7 +28,6 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.entity.PartitionEntity;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.packages.PackageBinaryRequestDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;

View File

@ -1,9 +1,16 @@
package ca.uhn.fhir.jpa.partition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.api.AddProfileTagEnum;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.ElementsSupportEnum;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import java.io.IOException;
import java.io.InputStream;
@ -11,6 +18,12 @@ import java.io.Reader;
import java.nio.charset.Charset;
import java.util.List;
/**
* A default RequestDetails implementation that can be used for system calls to
* Resource DAO methods when partitioning is enabled. Using a SystemRequestDetails
* instance for system calls will ensure that any resource queries or updates will
* use the DEFAULT partition when partitioninbg is enabled.
*/
public class SystemRequestDetails extends RequestDetails {
public SystemRequestDetails(IInterceptorBroadcaster theInterceptorBroadcaster) {
super(theInterceptorBroadcaster);
@ -63,11 +76,59 @@ public class SystemRequestDetails extends RequestDetails {
@Override
public IRestfulServerDefaults getServer() {
return null;
return new MyRestfulServerDefaults();
}
@Override
public String getServerBaseForRequest() {
return null;
}
private static class MyRestfulServerDefaults implements IRestfulServerDefaults {
@Override
public AddProfileTagEnum getAddProfileTag() {
return null;
}
@Override
public EncodingEnum getDefaultResponseEncoding() {
return null;
}
@Override
public ETagSupportEnum getETagSupport() {
return null;
}
@Override
public ElementsSupportEnum getElementsSupport() {
return null;
}
@Override
public FhirContext getFhirContext() {
return null;
}
@Override
public List<IServerInterceptor> getInterceptors_() {
return null;
}
@Override
public IPagingProvider getPagingProvider() {
return null;
}
@Override
public boolean isDefaultPrettyPrint() {
return false;
}
@Override
public IInterceptorService getInterceptorService() {
return null;
}
}
}

View File

@ -2,6 +2,8 @@ package ca.uhn.fhir.jpa.packages;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.INpmPackageDao;
import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao;
@ -10,13 +12,18 @@ import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.model.entity.NpmPackageEntity;
import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity;
import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
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.TokenParam;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.test.utilities.ProxyUtil;
import ca.uhn.fhir.util.JsonUtil;
@ -63,6 +70,8 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NpmR4Test extends BaseJpaR4Test {
@ -81,6 +90,10 @@ public class NpmR4Test extends BaseJpaR4Test {
@Autowired
private INpmPackageVersionResourceDao myPackageVersionResourceDao;
private FakeNpmServlet myFakeNpmServlet;
@Autowired
private IInterceptorService myInterceptorService;
@Autowired
private RequestTenantPartitionInterceptor myRequestTenantPartitionInterceptor;
@BeforeEach
public void before() throws Exception {
@ -105,6 +118,8 @@ public class NpmR4Test extends BaseJpaR4Test {
public void after() throws Exception {
JettyUtil.closeServer(myServer);
myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences());
myPartitionSettings.setPartitioningEnabled(false);
myInterceptorService.unregisterInterceptor(myRequestTenantPartitionInterceptor);
}
@ -273,6 +288,47 @@ public class NpmR4Test extends BaseJpaR4Test {
}
@Test
public void testInstallR4Package_NonConformanceResources_Partitioned() throws Exception {
myPartitionSettings.setPartitioningEnabled(true);
myInterceptorService.registerInterceptor(myRequestTenantPartitionInterceptor);
myDaoConfig.setAllowExternalReferences(true);
byte[] bytes = loadClasspathBytes("/packages/test-organizations-package.tgz");
myFakeNpmServlet.myResponses.put("/test-organizations/1.0.0", bytes);
List<String> resourceList = new ArrayList<>();
resourceList.add("Organization");
PackageInstallationSpec spec = new PackageInstallationSpec().setName("test-organizations").setVersion("1.0.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL);
spec.setInstallResourceTypes(resourceList);
PackageInstallOutcomeJson outcome = igInstaller.install(spec);
assertEquals(3, outcome.getResourcesInstalled().get("Organization"));
// Be sure no further communication with the server
JettyUtil.closeServer(myServer);
// Search for the installed resources
mySrd = mock(ServletRequestDetails.class);
when(mySrd.getTenantId()).thenReturn(JpaConstants.DEFAULT_PARTITION_NAME);
when(mySrd.getServer()).thenReturn(mock(RestfulServer.class));
when(mySrd.getInterceptorBroadcaster()).thenReturn(mock(IInterceptorBroadcaster.class));
runInTransaction(() -> {
SearchParameterMap map = SearchParameterMap.newSynchronous();
map.add(Organization.SP_IDENTIFIER, new TokenParam("https://github.com/synthetichealth/synthea", "organization1"));
IBundleProvider result = myOrganizationDao.search(map, mySrd);
assertEquals(1, result.sizeOrThrowNpe());
map = SearchParameterMap.newSynchronous();
map.add(Organization.SP_IDENTIFIER, new TokenParam("https://github.com/synthetichealth/synthea", "organization2"));
result = myOrganizationDao.search(map, mySrd);
assertEquals(1, result.sizeOrThrowNpe());
map = SearchParameterMap.newSynchronous();
map.add(Organization.SP_IDENTIFIER, new TokenParam("https://github.com/synthetichealth/synthea", "organization3"));
result = myOrganizationDao.search(map, mySrd);
assertEquals(1, result.sizeOrThrowNpe());
});
}
@Test
public void testInstallR4Package_NoIdentifierNoUrl() throws Exception {
myDaoConfig.setAllowExternalReferences(true);