theResultList) {
@@ -142,26 +140,33 @@ public class DeleteConflictService {
}
public static void validateDeleteConflictsEmptyOrThrowException(FhirContext theFhirContext, DeleteConflictList theDeleteConflicts) {
- if (theDeleteConflicts.isEmpty()) {
- return;
- }
-
- IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(theFhirContext);
+ IBaseOperationOutcome oo = null;
String firstMsg = null;
for (DeleteConflict next : theDeleteConflicts) {
+
+ if (theDeleteConflicts.isResourceIdToIgnoreConflict(next.getTargetId())) {
+ continue;
+ }
+
String msg = "Unable to delete " +
next.getTargetId().toUnqualifiedVersionless().getValue() +
" because at least one resource has a reference to this resource. First reference found was resource " +
next.getSourceId().toUnqualifiedVersionless().getValue() +
" in path " +
next.getSourcePath();
+
if (firstMsg == null) {
firstMsg = msg;
+ oo = OperationOutcomeUtil.newInstance(theFhirContext);
}
OperationOutcomeUtil.addIssue(theFhirContext, oo, BaseHapiFhirDao.OO_SEVERITY_ERROR, msg, null, "processing");
}
+ if (firstMsg == null) {
+ return;
+ }
+
throw new ResourceVersionConflictException(firstMsg, oo);
}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.java
index fc8aa12388c..a2875454e06 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.java
@@ -71,6 +71,13 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Interceptor
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 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";
@@ -94,7 +101,7 @@ public class CascadingDeleteInterceptor {
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) {
ourLog.debug("Have delete conflicts: {}", theConflictList);
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptor.java
new file mode 100644
index 00000000000..e89261cb1b7
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptor.java
@@ -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.
+ *
+ * For example, suppose this interceptor is configured with a path of AuditEvent.entity.what
,
+ * and an AuditEvent resource exists in the repository that has a reference in that path to resource
+ * Patient/123
. Normally this reference would prevent the Patient resource from being deleted unless
+ * the AuditEvent was first deleted as well (or a cascading delete was used).
+ * With this interceptor in place, the Patient resource could be deleted, and the AuditEvent would remain intact.
+ *
+ */
+@Interceptor
+public class OverridePathBasedReferentialIntegrityForDeletesInterceptor {
+
+ private static final Logger ourLog = LoggerFactory.getLogger(OverridePathBasedReferentialIntegrityForDeletesInterceptor.class);
+ private final Set 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. AuditEvent.agent.who
+ */
+ 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 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 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;
+ }
+ }
+
+ }
+
+ }
+ }
+}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java
index 97cc8d26b42..1cac6951212 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java
@@ -99,7 +99,7 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test {
@Test
- public void testDeleteCircularReferenceInTransaction() throws IOException {
+ public void testDeleteCircularReferenceInTransaction() {
// Create two resources with a circular reference
Organization org1 = new Organization();
@@ -221,4 +221,12 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test {
}
+ @Test
+ public void testDeleteIgnoreReferentialIntegrityForPaths() {
+
+
+
+ }
+
+
}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/CascadingDeleteInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptorTest.java
similarity index 82%
rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/CascadingDeleteInterceptorR4Test.java
rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptorTest.java
index 231d1ebc2de..afa85cc0522 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/CascadingDeleteInterceptorR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptorTest.java
@@ -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.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.server.exceptions.ResourceGoneException;
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.Reference;
import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.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;
@Autowired
@@ -42,18 +41,13 @@ public class CascadingDeleteInterceptorR4Test extends BaseResourceProviderR4Test
private IInterceptorBroadcaster myInterceptorBroadcaster;
private IIdType myPatientId;
+ @Autowired
private CascadingDeleteInterceptor myDeleteInterceptor;
private IIdType myObservationId;
private IIdType myConditionId;
private IIdType myEncounterId;
-
- @Override
- @BeforeEach
- public void before() throws Exception {
- super.before();
-
- myDeleteInterceptor = new CascadingDeleteInterceptor(myFhirCtx, myDaoRegistry, myInterceptorBroadcaster);
- }
+ @Autowired
+ private OverridePathBasedReferentialIntegrityForDeletesInterceptor myOverridePathBasedReferentialIntegrityForDeletesInterceptor;
@Override
@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
public void testDeleteCascadingWithCircularReference() throws IOException {
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptorTest.java
new file mode 100644
index 00000000000..14363d30a1a
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptorTest.java
@@ -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);
+ }
+
+ }
+
+}