Disable referential integrity for some paths (#2025)

* Disable referential integrity for some paths

* Add changelog

* Remove unneeded file
This commit is contained in:
James Agnew 2020-08-09 07:09:37 -04:00 committed by GitHub
parent 0de0b88aa0
commit 8e0023c385
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 374 additions and 30 deletions

View File

@ -0,0 +1,5 @@
---
type: add
issue: 2025
title: "A new interceptor has been added for the JPA server that selectively allows resource deletions to proceed even if
there are valid references to the candidate for deletion from other resources that are not being deleted."

View File

@ -180,8 +180,24 @@ The ResponseSizeCapturingInterceptor can be used to capture the number of charac
# JPA Server: Allow Cascading Deletes # JPA Server: Allow Cascading Deletes
* [CascadingDeleteInterceptor JavaDoc](/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.html)
* [CascadingDeleteInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.java)
The CascadingDeleteInterceptor allows clients to request deletes be cascaded to other resources that contain incoming references. See [Cascading Deletes](/docs/server_jpa/configuration.html#cascading-deletes) for more information. The CascadingDeleteInterceptor allows clients to request deletes be cascaded to other resources that contain incoming references. See [Cascading Deletes](/docs/server_jpa/configuration.html#cascading-deletes) for more information.
<a name="overridepathbasedreferentialintegrityfordeletesinterceptor"/>
# JPA Server: Disable Referential Integrity for Some Paths
* [OverridePathBasedReferentialIntegrityForDeletesInterceptor JavaDoc](/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptor.html)
* [OverridePathBasedReferentialIntegrityForDeletesInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptor.java)
The OverridePathBasedReferentialIntegrityForDeletesInterceptor can be registered and configured to allow resources to be deleted even if other resources have outgoing references to the deleted resource. While it is generally a bad idea to allow deletion of resources that are referred to from other resources, there are circumstances where it is desirable. For example, if you have Provenance or AuditEvent resources that refer to a Patient resource that was created in error, you might want to alow the Patient to be deleted while leaving the Provenance and AuditEvent resources intact (including the now-invalid outgoing references to that Patient).
This interceptor uses FHIRPath expressions to indicate the resource paths that should not have referential integrity applied to them. For example, if this interceptor is configured with a path of `AuditEvent.agent.who`, a Patient resource would be allowed to be deleted even if one or more AuditEvents had references in that path to the given Patient (unless other resources also had references to the Patient).
# JPA Server: Retry on Version Conflicts # JPA Server: Retry on Version Conflicts
The UserRequestRetryVersionConflictsInterceptor allows clients to request that the server avoid version conflicts (HTTP 409) when two concurrent client requests attempt to modify the same resource. See [Version Conflicts](/docs/server_jpa/configuration.html#retry-on-version-conflict) for more information. The UserRequestRetryVersionConflictsInterceptor allows clients to request that the server avoid version conflicts (HTTP 409) when two concurrent client requests attempt to modify the same resource. See [Version Conflicts](/docs/server_jpa/configuration.html#retry-on-version-conflict) for more information.

View File

@ -34,6 +34,7 @@ import java.util.function.Predicate;
public class DeleteConflictList implements Iterable<DeleteConflict> { public class DeleteConflictList implements Iterable<DeleteConflict> {
private final List<DeleteConflict> myList = new ArrayList<>(); private final List<DeleteConflict> myList = new ArrayList<>();
private final Set<String> myResourceIdsMarkedForDeletion; private final Set<String> myResourceIdsMarkedForDeletion;
private final Set<String> myResourceIdsToIgnoreConflict;
private int myRemoveModCount; private int myRemoveModCount;
/** /**
@ -41,6 +42,7 @@ public class DeleteConflictList implements Iterable<DeleteConflict> {
*/ */
public DeleteConflictList() { public DeleteConflictList() {
myResourceIdsMarkedForDeletion = new HashSet<>(); myResourceIdsMarkedForDeletion = new HashSet<>();
myResourceIdsToIgnoreConflict = new HashSet<>();
} }
/** /**
@ -49,6 +51,7 @@ public class DeleteConflictList implements Iterable<DeleteConflict> {
*/ */
public DeleteConflictList(DeleteConflictList theParentList) { public DeleteConflictList(DeleteConflictList theParentList) {
myResourceIdsMarkedForDeletion = theParentList.myResourceIdsMarkedForDeletion; myResourceIdsMarkedForDeletion = theParentList.myResourceIdsMarkedForDeletion;
myResourceIdsToIgnoreConflict = theParentList.myResourceIdsToIgnoreConflict;
} }
@ -64,6 +67,18 @@ public class DeleteConflictList implements Iterable<DeleteConflict> {
myResourceIdsMarkedForDeletion.add(theIdType.toUnqualifiedVersionless().getValue()); myResourceIdsMarkedForDeletion.add(theIdType.toUnqualifiedVersionless().getValue());
} }
public boolean isResourceIdToIgnoreConflict(IIdType theIdType) {
Validate.notNull(theIdType);
Validate.notBlank(theIdType.toUnqualifiedVersionless().getValue());
return myResourceIdsToIgnoreConflict.contains(theIdType.toUnqualifiedVersionless().getValue());
}
public void setResourceIdToIgnoreConflict(IIdType theIdType) {
Validate.notNull(theIdType);
Validate.notBlank(theIdType.toUnqualifiedVersionless().getValue());
myResourceIdsToIgnoreConflict.add(theIdType.toUnqualifiedVersionless().getValue());
}
public void add(DeleteConflict theDeleteConflict) { public void add(DeleteConflict theDeleteConflict) {
myList.add(theDeleteConflict); myList.add(theDeleteConflict);
} }

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer; import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.executor.InterceptorService; import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
@ -24,7 +25,9 @@ import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
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.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService; import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager; import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager;
import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc; import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc;
@ -167,6 +170,12 @@ public abstract class BaseConfig {
return new BatchJobSubmitterImpl(); return new BatchJobSubmitterImpl();
} }
@Lazy
@Bean
public CascadingDeleteInterceptor cascadingDeleteInterceptor(FhirContext theFhirContext, DaoRegistry theDaoRegistry, IInterceptorBroadcaster theInterceptorBroadcaster) {
return new CascadingDeleteInterceptor(theFhirContext, theDaoRegistry, theInterceptorBroadcaster);
}
/** /**
* This method should be overridden to provide an actual completed * This method should be overridden to provide an actual completed
* bean, but it provides a partially completed entity manager * bean, but it provides a partially completed entity manager
@ -295,6 +304,12 @@ public abstract class BaseConfig {
return new HapiFhirHibernateJpaDialect(fhirContext().getLocalizer()); return new HapiFhirHibernateJpaDialect(fhirContext().getLocalizer());
} }
@Bean
@Lazy
public OverridePathBasedReferentialIntegrityForDeletesInterceptor overridePathBasedReferentialIntegrityForDeletesInterceptor() {
return new OverridePathBasedReferentialIntegrityForDeletesInterceptor();
}
@Bean @Bean
public IRequestPartitionHelperSvc requestPartitionHelperService() { public IRequestPartitionHelperSvc requestPartitionHelperService() {
return new RequestPartitionHelperSvc(); return new RequestPartitionHelperSvc();

View File

@ -44,27 +44,25 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.List; import java.util.List;
@Service @Service
public class DeleteConflictService { public class DeleteConflictService {
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictService.class);
public static final int FIRST_QUERY_RESULT_COUNT = 1; public static final int FIRST_QUERY_RESULT_COUNT = 1;
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictService.class);
public static int MAX_RETRY_ATTEMPTS = 10; public static int MAX_RETRY_ATTEMPTS = 10;
public static String MAX_RETRY_ATTEMPTS_EXCEEDED_MSG = "Requested delete operation stopped before all conflicts were handled. May need to increase the configured Maximum Delete Conflict Query Count."; public static String MAX_RETRY_ATTEMPTS_EXCEEDED_MSG = "Requested delete operation stopped before all conflicts were handled. May need to increase the configured Maximum Delete Conflict Query Count.";
@Autowired
protected IResourceLinkDao myResourceLinkDao;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired @Autowired
DeleteConflictFinderService myDeleteConflictFinderService; DeleteConflictFinderService myDeleteConflictFinderService;
@Autowired @Autowired
DaoConfig myDaoConfig; DaoConfig myDaoConfig;
@Autowired @Autowired
protected IResourceLinkDao myResourceLinkDao;
@Autowired
private FhirContext myFhirContext; private FhirContext myFhirContext;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
public int validateOkToDelete(DeleteConflictList theDeleteConflicts, ResourceTable theEntity, boolean theForValidate, RequestDetails theRequest, TransactionDetails theTransactionDetails) { public int validateOkToDelete(DeleteConflictList theDeleteConflicts, ResourceTable theEntity, boolean theForValidate, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
@ -142,26 +140,33 @@ public class DeleteConflictService {
} }
public static void validateDeleteConflictsEmptyOrThrowException(FhirContext theFhirContext, DeleteConflictList theDeleteConflicts) { public static void validateDeleteConflictsEmptyOrThrowException(FhirContext theFhirContext, DeleteConflictList theDeleteConflicts) {
if (theDeleteConflicts.isEmpty()) { IBaseOperationOutcome oo = null;
return;
}
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(theFhirContext);
String firstMsg = null; String firstMsg = null;
for (DeleteConflict next : theDeleteConflicts) { for (DeleteConflict next : theDeleteConflicts) {
if (theDeleteConflicts.isResourceIdToIgnoreConflict(next.getTargetId())) {
continue;
}
String msg = "Unable to delete " + String msg = "Unable to delete " +
next.getTargetId().toUnqualifiedVersionless().getValue() + next.getTargetId().toUnqualifiedVersionless().getValue() +
" because at least one resource has a reference to this resource. First reference found was resource " + " because at least one resource has a reference to this resource. First reference found was resource " +
next.getSourceId().toUnqualifiedVersionless().getValue() + next.getSourceId().toUnqualifiedVersionless().getValue() +
" in path " + " in path " +
next.getSourcePath(); next.getSourcePath();
if (firstMsg == null) { if (firstMsg == null) {
firstMsg = msg; firstMsg = msg;
oo = OperationOutcomeUtil.newInstance(theFhirContext);
} }
OperationOutcomeUtil.addIssue(theFhirContext, oo, BaseHapiFhirDao.OO_SEVERITY_ERROR, msg, null, "processing"); OperationOutcomeUtil.addIssue(theFhirContext, oo, BaseHapiFhirDao.OO_SEVERITY_ERROR, msg, null, "processing");
} }
if (firstMsg == null) {
return;
}
throw new ResourceVersionConflictException(firstMsg, oo); throw new ResourceVersionConflictException(firstMsg, oo);
} }

View File

@ -71,6 +71,13 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Interceptor @Interceptor
public class CascadingDeleteInterceptor { public class CascadingDeleteInterceptor {
/*
* We keep the orders for the various handlers of {@link Pointcut#STORAGE_PRESTORAGE_DELETE_CONFLICTS} in one place
* so it's easy to compare them
*/
public static final int OVERRIDE_PATH_BASED_REF_INTEGRITY_INTERCEPTOR_ORDER = 0;
public static final int CASCADING_DELETE_INTERCEPTOR_ORDER = 1;
private static final Logger ourLog = LoggerFactory.getLogger(CascadingDeleteInterceptor.class); private static final Logger ourLog = LoggerFactory.getLogger(CascadingDeleteInterceptor.class);
private static final String CASCADED_DELETES_KEY = CascadingDeleteInterceptor.class.getName() + "_CASCADED_DELETES_KEY"; private static final String CASCADED_DELETES_KEY = CascadingDeleteInterceptor.class.getName() + "_CASCADED_DELETES_KEY";
private static final String CASCADED_DELETES_FAILED_KEY = CascadingDeleteInterceptor.class.getName() + "_CASCADED_DELETES_FAILED_KEY"; private static final String CASCADED_DELETES_FAILED_KEY = CascadingDeleteInterceptor.class.getName() + "_CASCADED_DELETES_FAILED_KEY";
@ -94,7 +101,7 @@ public class CascadingDeleteInterceptor {
myFhirContext = theFhirContext; myFhirContext = theFhirContext;
} }
@Hook(Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS) @Hook(value = Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS, order = CASCADING_DELETE_INTERCEPTOR_ORDER)
public DeleteConflictOutcome handleDeleteConflicts(DeleteConflictList theConflictList, RequestDetails theRequest, TransactionDetails theTransactionDetails) { public DeleteConflictOutcome handleDeleteConflicts(DeleteConflictList theConflictList, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
ourLog.debug("Have delete conflicts: {}", theConflictList); ourLog.debug("Have delete conflicts: {}", theConflictList);

View File

@ -0,0 +1,107 @@
package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.fhirpath.IFhirPath;
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.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.model.primitive.IdDt;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* This JPA interceptor can be configured with a collection of FHIRPath expressions, and will disable
* referential integrity for target resources at those paths.
* <p>
* For example, suppose this interceptor is configured with a path of <code>AuditEvent.entity.what</code>,
* and an AuditEvent resource exists in the repository that has a reference in that path to resource
* <code>Patient/123</code>. Normally this reference would prevent the Patient resource from being deleted unless
* the AuditEvent was first deleted as well (or a <a href="/hapi-fhir/docs/server_jpa/configuration.html#cascading-deletes">cascading delete</a> was used).
* With this interceptor in place, the Patient resource could be deleted, and the AuditEvent would remain intact.
* </p>
*/
@Interceptor
public class OverridePathBasedReferentialIntegrityForDeletesInterceptor {
private static final Logger ourLog = LoggerFactory.getLogger(OverridePathBasedReferentialIntegrityForDeletesInterceptor.class);
private final Set<String> myPaths = new HashSet<>();
@Autowired
private FhirContext myFhirContext;
@Autowired
private DaoRegistry myDaoRegistry;
/**
* Constructor
*/
public OverridePathBasedReferentialIntegrityForDeletesInterceptor() {
super();
}
/**
* Adds a FHIRPath expression indicating a resource path that should be ignored when considering referential
* integrity for deletes.
*
* @param thePath The FHIRPath expression, e.g. <code>AuditEvent.agent.who</code>
*/
public void addPath(String thePath) {
getPaths().add(thePath);
}
/**
* Remove all paths registered to this interceptor
*/
public void clearPaths() {
getPaths().clear();
}
/**
* Returns the paths that will be considered by this interceptor
*
* @see #addPath(String)
*/
private Set<String> getPaths() {
return myPaths;
}
/**
* Interceptor hook method. Do not invoke directly.
*/
@Hook(value = Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS, order = CascadingDeleteInterceptor.OVERRIDE_PATH_BASED_REF_INTEGRITY_INTERCEPTOR_ORDER)
public void handleDeleteConflicts(DeleteConflictList theDeleteConflictList) {
for (DeleteConflict nextConflict : theDeleteConflictList) {
ourLog.info("Ignoring referential integrity deleting {} - Referred to from {} at path {}", nextConflict.getTargetId(), nextConflict.getSourceId(), nextConflict.getSourcePath());
IdDt sourceId = nextConflict.getSourceId();
IdDt targetId = nextConflict.getTargetId();
String targetIdValue = targetId.toVersionless().getValue();
IBaseResource sourceResource = myDaoRegistry.getResourceDao(sourceId.getResourceType()).read(sourceId);
IFhirPath fhirPath = myFhirContext.newFhirPath();
for (String nextPath : myPaths) {
List<IBaseReference> selections = fhirPath.evaluate(sourceResource, nextPath, IBaseReference.class);
for (IBaseReference nextSelection : selections) {
String selectionTargetValue = nextSelection.getReferenceElement().toVersionless().getValue();
if (Objects.equals(targetIdValue, selectionTargetValue)) {
theDeleteConflictList.setResourceIdToIgnoreConflict(nextConflict.getTargetId());
break;
}
}
}
}
}
}

View File

@ -99,7 +99,7 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test {
@Test @Test
public void testDeleteCircularReferenceInTransaction() throws IOException { public void testDeleteCircularReferenceInTransaction() {
// Create two resources with a circular reference // Create two resources with a circular reference
Organization org1 = new Organization(); Organization org1 = new Organization();
@ -221,4 +221,12 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test {
} }
@Test
public void testDeleteIgnoreReferentialIntegrityForPaths() {
}
} }

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.provider.r4; package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
@ -20,7 +20,6 @@ import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -31,9 +30,9 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
public class CascadingDeleteInterceptorR4Test extends BaseResourceProviderR4Test { public class CascadingDeleteInterceptorTest extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CascadingDeleteInterceptorR4Test.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CascadingDeleteInterceptorTest.class);
private IIdType myDiagnosticReportId; private IIdType myDiagnosticReportId;
@Autowired @Autowired
@ -42,18 +41,13 @@ public class CascadingDeleteInterceptorR4Test extends BaseResourceProviderR4Test
private IInterceptorBroadcaster myInterceptorBroadcaster; private IInterceptorBroadcaster myInterceptorBroadcaster;
private IIdType myPatientId; private IIdType myPatientId;
@Autowired
private CascadingDeleteInterceptor myDeleteInterceptor; private CascadingDeleteInterceptor myDeleteInterceptor;
private IIdType myObservationId; private IIdType myObservationId;
private IIdType myConditionId; private IIdType myConditionId;
private IIdType myEncounterId; private IIdType myEncounterId;
@Autowired
@Override private OverridePathBasedReferentialIntegrityForDeletesInterceptor myOverridePathBasedReferentialIntegrityForDeletesInterceptor;
@BeforeEach
public void before() throws Exception {
super.before();
myDeleteInterceptor = new CascadingDeleteInterceptor(myFhirCtx, myDaoRegistry, myInterceptorBroadcaster);
}
@Override @Override
@AfterEach @AfterEach
@ -161,6 +155,37 @@ public class CascadingDeleteInterceptorR4Test extends BaseResourceProviderR4Test
} }
} }
@Test
public void testDeleteCascadingWithOverridePathBasedReferentialIntegrityForDeletesInterceptorAlsoRegistered() throws IOException {
ourRestServer.getInterceptorService().registerInterceptor(myOverridePathBasedReferentialIntegrityForDeletesInterceptor);
try {
createResources();
ourRestServer.getInterceptorService().registerInterceptor(myDeleteInterceptor);
HttpDelete delete = new HttpDelete(ourServerBase + "/" + myPatientId.getValue() + "?" + Constants.PARAMETER_CASCADE_DELETE + "=" + Constants.CASCADE_DELETE + "&_pretty=true");
delete.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON_NEW);
try (CloseableHttpResponse response = ourHttpClient.execute(delete)) {
assertEquals(200, response.getStatusLine().getStatusCode());
String deleteResponse = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", deleteResponse);
assertThat(deleteResponse, containsString("Cascaded delete to "));
}
try {
ourLog.info("Reading {}", myPatientId);
myClient.read().resource(Patient.class).withId(myPatientId).execute();
fail();
} catch (ResourceGoneException e) {
// good
}
} finally {
ourRestServer.getInterceptorService().unregisterInterceptor(myOverridePathBasedReferentialIntegrityForDeletesInterceptor);
}
}
@Test @Test
public void testDeleteCascadingWithCircularReference() throws IOException { public void testDeleteCascadingWithCircularReference() throws IOException {

View File

@ -0,0 +1,141 @@
package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import org.hl7.fhir.r4.model.AuditEvent;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class OverridePathBasedReferentialIntegrityForDeletesInterceptorTest extends BaseJpaR4Test {
@Autowired
private OverridePathBasedReferentialIntegrityForDeletesInterceptor mySvc;
@Autowired
private CascadingDeleteInterceptor myCascadingDeleteInterceptor;
@AfterEach
public void after() {
myInterceptorRegistry.unregisterInterceptor(mySvc);
mySvc.clearPaths();
}
@Test
public void testDeleteBlockedIfNoInterceptorInPlace() {
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myPatientDao.update(patient);
AuditEvent audit = new AuditEvent();
audit.setId("A");
audit.addAgent().getWho().setReference("Patient/P");
myAuditEventDao.update(audit);
try {
myPatientDao.delete(new IdType("Patient/P"));
fail();
} catch (ResourceVersionConflictException e) {
// good
}
}
@Test
public void testAllowDelete() {
mySvc.addPath("AuditEvent.agent.who");
myInterceptorRegistry.registerInterceptor(mySvc);
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myPatientDao.update(patient);
AuditEvent audit = new AuditEvent();
audit.setId("A");
audit.addAgent().getWho().setReference("Patient/P");
myAuditEventDao.update(audit);
// Delete should proceed
myPatientDao.delete(new IdType("Patient/P"));
// Make sure we're deleted
try {
myPatientDao.read(new IdType("Patient/P"));
fail();
} catch (ResourceGoneException e) {
// good
}
// Search should still work
IBundleProvider searchOutcome = myAuditEventDao.search(SearchParameterMap.newSynchronous(AuditEvent.SP_AGENT, new ReferenceParam("Patient/P")));
assertEquals(1, searchOutcome.size());
}
@Test
public void testWrongPath() {
mySvc.addPath("AuditEvent.identifier");
mySvc.addPath("Patient.agent.who");
myInterceptorRegistry.registerInterceptor(mySvc);
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myPatientDao.update(patient);
AuditEvent audit = new AuditEvent();
audit.setId("A");
audit.addAgent().getWho().setReference("Patient/P");
myAuditEventDao.update(audit);
// Delete should proceed
try {
myPatientDao.delete(new IdType("Patient/P"));
fail();
} catch (ResourceVersionConflictException e) {
// good
}
}
@Test
public void testCombineWithCascadeDeleteInterceptor() {
try {
myInterceptorRegistry.registerInterceptor(myCascadingDeleteInterceptor);
mySvc.addPath("AuditEvent.agent.who");
myInterceptorRegistry.registerInterceptor(mySvc);
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myPatientDao.update(patient);
AuditEvent audit = new AuditEvent();
audit.setId("A");
audit.addAgent().getWho().setReference("Patient/P");
myAuditEventDao.update(audit);
// Delete should proceed
myPatientDao.delete(new IdType("Patient/P"));
} finally {
myInterceptorRegistry.unregisterInterceptor(myCascadingDeleteInterceptor);
}
}
}