This commit is contained in:
Tadgh 2021-04-09 13:07:15 -04:00
parent 28c917b7a2
commit ac8e012892
3 changed files with 226 additions and 3 deletions

View File

@ -1513,10 +1513,10 @@ public enum Pointcut implements IPointcut {
/**
* <b>Storage Hook:</b>
* Invoked before a resource will be deleted
* Invoked after all entries in a transaction bundle have been executed
* <p>
* Hooks will have access to the contents of the resource being deleted
* but should not make any changes as storage has already occurred
* Hooks will have access to the original bundle, as well as all the deferred interceptor broadcasts related to the
* processing of the transaction bundle
* </p>
* Hooks may accept the following parameters:
* <ul>
@ -1537,6 +1537,9 @@ public enum Pointcut implements IPointcut {
* <li>
* ca.uhn.fhir.rest.api.server.storage.TransactionDetails - The outer transaction details object (since 5.0.0)
* </li>
* <li>
* ca.uhn.fhir.rest.api.server.storage.DeferredInterceptorBroadcasts- A collection of pointcut invocations and their parameters which were deferred.
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.

View File

@ -48,6 +48,7 @@ import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.DeferredInterceptorBroadcasts;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
@ -1025,6 +1026,12 @@ public abstract class BaseTransactionProcessor {
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, nextPointcut, nextParams);
}
DeferredInterceptorBroadcasts deferredInterceptorBroadcasts = new DeferredInterceptorBroadcasts(deferredBroadcastEvents);
HookParams params = new HookParams()
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(DeferredInterceptorBroadcasts.class, deferredInterceptorBroadcasts);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_TRANSACTION_PROCESSED, params);
theTransactionDetails.deferredBroadcastProcessingFinished();

View File

@ -0,0 +1,213 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.test.concurrency.PointcutLatch;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.Observation;
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;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.jupiter.api.Assertions.fail;
public class TransactionHookTest extends BaseJpaR4SystemTest {
@AfterEach
public void after() {
myDaoConfig.setEnforceReferentialIntegrityOnDelete(true);
}
PointcutLatch myPointcutLatch = new PointcutLatch(Pointcut.STORAGE_TRANSACTION_PROCESSED);
@Autowired
private IInterceptorService myInterceptorService;
@BeforeEach
public void beforeEach() {
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_TRANSACTION_PROCESSED, myPointcutLatch);
}
@Test
public void testHookShouldContainParamsForAllCreateUpdateDeleteInvocations() {
final Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless();
final Observation obs2 = new Observation();
obs2.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs2id = myObservationDao.create(obs2).getId().toUnqualifiedVersionless();
final DiagnosticReport rpt = new DiagnosticReport();
rpt.addResult(new Reference(obs2id));
IIdType rptId = myDiagnosticReportDao.create(rpt).getId().toUnqualifiedVersionless();
myObservationDao.read(obs1id);
myObservationDao.read(obs2id);
myDiagnosticReportDao.read(rptId);
Bundle b = new Bundle();
b.addEntry().getRequest().setMethod(Bundle.HTTPVerb.DELETE).setUrl(obs2id.getValue());
try {
mySystemDao.transaction(mySrd, b);
fail();
} catch (ResourceVersionConflictException e) {
// good, transaction should not succeed because DiagnosticReport has a reference to the obs2
}
}
@Test
public void testDeleteInTransactionShouldSucceedWhenReferencesAreAlsoRemoved() {
final Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless();
final Observation obs2 = new Observation();
obs2.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs2id = myObservationDao.create(obs2).getId().toUnqualifiedVersionless();
final DiagnosticReport rpt = new DiagnosticReport();
rpt.addResult(new Reference(obs2id));
IIdType rptId = myDiagnosticReportDao.create(rpt).getId().toUnqualifiedVersionless();
myObservationDao.read(obs1id);
myObservationDao.read(obs2id);
myDiagnosticReportDao.read(rptId);
Bundle b = new Bundle();
b.addEntry().getRequest().setMethod(Bundle.HTTPVerb.DELETE).setUrl(rptId.getValue());
b.addEntry().getRequest().setMethod(Bundle.HTTPVerb.DELETE).setUrl(obs2id.getValue());
try {
// transaction should succeed because the DiagnosticReport which references obs2 is also deleted
mySystemDao.transaction(mySrd, b);
} catch (ResourceVersionConflictException e) {
fail();
}
}
@Test
public void testDeleteWithHas_SourceModifiedToNoLongerIncludeReference() {
Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless();
Observation obs2 = new Observation();
obs2.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs2id = myObservationDao.create(obs2).getId().toUnqualifiedVersionless();
DiagnosticReport rpt = new DiagnosticReport();
rpt.addIdentifier().setSystem("foo").setValue("IDENTIFIER");
rpt.addResult(new Reference(obs2id));
IIdType rptId = myDiagnosticReportDao.create(rpt).getId().toUnqualifiedVersionless();
myObservationDao.read(obs1id);
myObservationDao.read(obs2id);
rpt = new DiagnosticReport();
rpt.addIdentifier().setSystem("foo").setValue("IDENTIFIER");
Bundle b = new Bundle();
b.addEntry().getRequest().setMethod(Bundle.HTTPVerb.DELETE).setUrl("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER");
b.addEntry().setResource(rpt).getRequest().setMethod(Bundle.HTTPVerb.PUT).setUrl("DiagnosticReport?identifier=foo|IDENTIFIER");
mySystemDao.transaction(mySrd, b);
myObservationDao.read(obs1id);
try {
myObservationDao.read(obs2id);
fail();
} catch (ResourceGoneException e) {
// good
}
rpt = myDiagnosticReportDao.read(rptId);
assertThat(rpt.getResult(), empty());
}
@Test
public void testDeleteWithId_SourceModifiedToNoLongerIncludeReference() {
Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless();
Observation obs2 = new Observation();
obs2.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs2id = myObservationDao.create(obs2).getId().toUnqualifiedVersionless();
DiagnosticReport rpt = new DiagnosticReport();
rpt.addResult(new Reference(obs1id));
IIdType rptId = myDiagnosticReportDao.create(rpt).getId().toUnqualifiedVersionless();
myObservationDao.read(obs1id);
myObservationDao.read(obs2id);
rpt = new DiagnosticReport();
rpt.addResult(new Reference(obs2id));
Bundle b = new Bundle();
b.addEntry().getRequest().setMethod(Bundle.HTTPVerb.DELETE).setUrl(obs1id.getValue());
b.addEntry().setResource(rpt).getRequest().setMethod(Bundle.HTTPVerb.PUT).setUrl(rptId.getValue());
mySystemDao.transaction(mySrd, b);
myObservationDao.read(obs2id);
myDiagnosticReportDao.read(rptId);
try {
myObservationDao.read(obs1id);
fail();
} catch (ResourceGoneException e) {
// good
}
}
@Test
public void testDeleteWithHas_SourceModifiedToStillIncludeReference() {
Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless();
Observation obs2 = new Observation();
obs2.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs2id = myObservationDao.create(obs2).getId().toUnqualifiedVersionless();
DiagnosticReport rpt = new DiagnosticReport();
rpt.addIdentifier().setSystem("foo").setValue("IDENTIFIER");
rpt.addResult(new Reference(obs2id));
IIdType rptId = myDiagnosticReportDao.create(rpt).getId().toUnqualifiedVersionless();
myObservationDao.read(obs1id);
myObservationDao.read(obs2id);
rpt = new DiagnosticReport();
rpt.addIdentifier().setSystem("foo").setValue("IDENTIFIER");
rpt.addResult(new Reference(obs2id));
Bundle b = new Bundle();
b.addEntry().getRequest().setMethod(Bundle.HTTPVerb.DELETE).setUrl("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER");
b.addEntry().setResource(rpt).getRequest().setMethod(Bundle.HTTPVerb.PUT).setUrl("DiagnosticReport?identifier=foo|IDENTIFIER");
try {
mySystemDao.transaction(mySrd, b);
fail();
} catch (ResourceVersionConflictException e ) {
assertThat(e.getMessage(), matchesPattern("Unable to delete Observation/[0-9]+ because at least one resource has a reference to this resource. First reference found was resource DiagnosticReport/[0-9]+ in path DiagnosticReport.result"));
}
myObservationDao.read(obs1id);
myObservationDao.read(obs2id);
myDiagnosticReportDao.read(rptId);
}
}