diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java
index 7cae659a7d2..4797f2ef1c2 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java
@@ -67,8 +67,9 @@ public interface IInterceptorService extends IInterceptorBroadcaster {
* Unregister an interceptor. This method has no effect if the given interceptor is not already registered.
*
* @param theInterceptor The interceptor to unregister
+ * @return Returns true if the interceptor was found and removed
*/
- void unregisterInterceptor(Object theInterceptor);
+ boolean unregisterInterceptor(Object theInterceptor);
void registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor);
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java
index bd248475101..0c3e78c49bc 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java
@@ -1200,6 +1200,45 @@ public enum Pointcut {
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
+ /**
+ * Invoked before expunge is called on a resource.
+ *
+ * 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.
+ *
+ * Hooks may accept the following parameters:
+ *
+ *
java.util.concurrent.atomic.AtomicInteger - The counter holding the number of records deleted.
+ *
ca.uhn.fhir.model.primitive.IdDt - The id of the resource to be deleted. Note that version may be null.
+ *
+ * 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.
+ *
+ *
+ * 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.
+ *
+ *
+ *
+ * Hooks should return void.
+ *
+ */
+
+ STORAGE_PRESTORAGE_EXPUNGE_RESOURCE (
+ // Return type
+ void.class,
+ // Params
+ "java.util.concurrent.atomic.AtomicInteger",
+ "ca.uhn.fhir.model.primitive.IdDt",
+ "ca.uhn.fhir.rest.api.server.RequestDetails",
+ "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
+ ),
+
/**
* Note that this is a performance tracing hook. Use with caution in production
* systems, since calling it may (or may not) carry a cost.
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java
index d6960c62006..1651249900e 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java
@@ -208,11 +208,12 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
}
@Override
- public void unregisterInterceptor(Object theInterceptor) {
+ public boolean unregisterInterceptor(Object theInterceptor) {
synchronized (myRegistryMutex) {
- myInterceptors.removeIf(t -> t == theInterceptor);
- myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
- myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
+ boolean removed = myInterceptors.removeIf(t -> t == theInterceptor);
+ removed |= myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
+ removed |= myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
+ return removed;
}
}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeRun.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeOperation.java
similarity index 75%
rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeRun.java
rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeOperation.java
index 018645b9d09..b03f381c8c8 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeRun.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeOperation.java
@@ -20,9 +20,15 @@ package ca.uhn.fhir.jpa.dao.expunge;
* #L%
*/
+import ca.uhn.fhir.interceptor.api.HookParams;
+import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
+import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.jpa.util.ExpungeOutcome;
+import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
+import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.RequestDetails;
+import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -31,20 +37,21 @@ import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
+import javax.persistence.Id;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@Scope("prototype")
-public class ExpungeRun implements Callable {
+public class ExpungeOperation implements Callable {
private static final Logger ourLog = LoggerFactory.getLogger(ExpungeService.class);
- @Autowired
- private PlatformTransactionManager myPlatformTransactionManager;
@Autowired
private IResourceExpungeService myExpungeDaoService;
@Autowired
private PartitionRunner myPartitionRunner;
+ @Autowired
+ protected IInterceptorBroadcaster myInterceptorBroadcaster;
private final String myResourceName;
private final Long myResourceId;
@@ -53,7 +60,7 @@ public class ExpungeRun implements Callable {
private final RequestDetails myRequestDetails;
private final AtomicInteger myRemainingCount;
- public ExpungeRun(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails) {
+ public ExpungeOperation(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails) {
myResourceName = theResourceName;
myResourceId = theResourceId;
myVersion = theVersion;
@@ -64,6 +71,15 @@ public class ExpungeRun implements Callable {
@Override
public ExpungeOutcome call() {
+ final IdDt id;
+
+ callHooks();
+
+ if (expungeLimitReached()) {
+ return expungeOutcome();
+ }
+
+
if (myExpungeOptions.isExpungeDeletedResources() && myVersion == null) {
expungeDeletedResources();
if (expungeLimitReached()) {
@@ -81,6 +97,30 @@ public class ExpungeRun implements Callable {
return expungeOutcome();
}
+ private void callHooks() {
+ final AtomicInteger counter = new AtomicInteger();
+
+ if (myResourceId == null) {
+ return;
+ }
+ IdDt id;
+ if (myVersion == null) {
+ id = new IdDt(myResourceName, myResourceId);
+ } else {
+ id = new IdDt(myResourceName, myResourceId.toString(), myVersion.toString());
+ }
+
+ // Notify Interceptors about pre-action call
+ HookParams hooks = new HookParams()
+ .add(AtomicInteger.class, counter)
+ .add(IdDt.class, id)
+ .add(RequestDetails.class, myRequestDetails)
+ .addIfMatchesType(ServletRequestDetails.class, myRequestDetails);
+ JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequestDetails, Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, hooks);
+
+ myRemainingCount.addAndGet(-1 * counter.get());
+ }
+
private void expungeDeletedResources() {
Slice resourceIds = findHistoricalVersionsOfDeletedResources();
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeService.java
index 0f4ff4e7be2..238862cbd44 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeService.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeService.java
@@ -44,7 +44,7 @@ public abstract class ExpungeService {
private IResourceExpungeService myExpungeDaoService;
@Lookup
- protected abstract ExpungeRun getExpungeRun(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails);
+ protected abstract ExpungeOperation getExpungeOperation(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails);
public ExpungeOutcome expunge(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions, RequestDetails theRequest) {
ourLog.info("Expunge: ResourceName[{}] Id[{}] Version[{}] Options[{}]", theResourceName, theResourceId, theVersion, theExpungeOptions);
@@ -63,8 +63,8 @@ public abstract class ExpungeService {
}
}
- ExpungeRun expungeRun = getExpungeRun(theResourceName, theResourceId, theVersion, theExpungeOptions, theRequest);
- return expungeRun.call();
+ ExpungeOperation expungeOperation = getExpungeOperation(theResourceName, theResourceId, theVersion, theExpungeOptions, theRequest);
+ return expungeOperation.call();
}
public void deleteAllSearchParams(Long theResourceId) {
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java
index 787073330c7..5b080348fff 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java
@@ -154,8 +154,8 @@ class ResourceExpungeService implements IResourceExpungeService {
}
}
- private void expungeCurrentVersionOfResource(Long myResourceId, AtomicInteger theRemainingCount) {
- ResourceTable resource = myResourceTableDao.findById(myResourceId).orElseThrow(IllegalStateException::new);
+ private void expungeCurrentVersionOfResource(Long theResourceId, AtomicInteger theRemainingCount) {
+ ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalStateException::new);
ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersion(resource.getId(), resource.getVersion());
if (currentVersion != null) {
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeHookTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeHookTest.java
new file mode 100644
index 00000000000..3d5f722e291
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeHookTest.java
@@ -0,0 +1,89 @@
+package ca.uhn.fhir.jpa.dao.expunge;
+
+import ca.uhn.fhir.interceptor.api.HookParams;
+import ca.uhn.fhir.interceptor.api.IInterceptorService;
+import ca.uhn.fhir.interceptor.api.Pointcut;
+import ca.uhn.fhir.jpa.config.TestDstu3Config;
+import ca.uhn.fhir.jpa.dao.DaoConfig;
+import ca.uhn.fhir.jpa.dao.IFhirResourceDaoPatient;
+import ca.uhn.fhir.jpa.model.concurrency.PointcutLatch;
+import ca.uhn.fhir.jpa.util.ExpungeOptions;
+import ca.uhn.fhir.model.primitive.IdDt;
+import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
+import org.hl7.fhir.dstu3.model.Patient;
+import org.hl7.fhir.instance.model.api.IIdType;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.*;
+
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = {TestDstu3Config.class})
+public class ExpungeHookTest {
+ @Autowired
+ private IFhirResourceDaoPatient myPatientDao;
+ @Autowired
+ private ExpungeService myExpungeService;
+ @Autowired
+ private IInterceptorService myInterceptorService;
+ @Autowired
+ private DaoConfig myDaoConfig;
+
+ PointcutLatch myEverythingLatch = new PointcutLatch(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING);
+ PointcutLatch myExpungeResourceLatch = new PointcutLatch(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE);
+
+ @Before
+ public void before() {
+ myDaoConfig.setExpungeEnabled(true);
+ myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING, myEverythingLatch);
+ myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, myExpungeResourceLatch);
+ }
+
+ @After
+ public void after() {
+ assertTrue(myInterceptorService.unregisterInterceptor(myEverythingLatch));
+ assertTrue(myInterceptorService.unregisterInterceptor(myExpungeResourceLatch));
+ myDaoConfig.setExpungeEnabled(new DaoConfig().isExpungeEnabled());
+ }
+
+ @Test
+ public void expungeEverythingHook() throws InterruptedException {
+ IIdType id = myPatientDao.create(new Patient()).getId();
+ assertNotNull(myPatientDao.read(id));
+ myEverythingLatch.setExpectedCount(1);
+ ExpungeOptions options = new ExpungeOptions();
+ options.setExpungeEverything(true);
+ myExpungeService.expunge(null, null, null, options, null);
+ myEverythingLatch.awaitExpected();
+ try {
+ myPatientDao.read(id);
+ fail();
+ } catch (ResourceNotFoundException e) {
+ assertThat(e.getMessage(), containsString("is not known"));
+ }
+ }
+
+ @Test
+ public void expungeResourceHook() throws InterruptedException {
+ IIdType expungeId = myPatientDao.create(new Patient()).getId();
+ assertNotNull(myPatientDao.read(expungeId));
+ myExpungeResourceLatch.setExpectedCount(1);
+ myPatientDao.delete(expungeId);
+
+ ExpungeOptions options = new ExpungeOptions();
+ options.setExpungeDeletedResources(true);
+ myExpungeService.expunge("Patient", expungeId.getIdPartAsLong(), expungeId.getVersionIdPartAsLong(), options, null);
+ HookParams hookParams = myExpungeResourceLatch.awaitExpected().get(0);
+ IdDt hookId = hookParams.get(IdDt.class);
+ assertEquals(expungeId.getValue(), hookId.getValue());
+ }
+}
diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/concurrency/FhirObjectPrinter.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/concurrency/FhirObjectPrinter.java
index 468384a7fbd..2d16df27bb9 100644
--- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/concurrency/FhirObjectPrinter.java
+++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/concurrency/FhirObjectPrinter.java
@@ -30,8 +30,10 @@ public class FhirObjectPrinter implements Function