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 fcac3de9935..e31009e9a26 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
@@ -1166,9 +1166,14 @@ public enum Pointcut {
/**
* Invoked before a resource is about to be expunged via the $expunge
operation.
*
+ * 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.
* org.hl7.fhir.instance.model.api.IIdType - The ID of the resource that is about to be deleted
* org.hl7.fhir.instance.model.api.IBaseResource - The resource that is about to be deleted
*
@@ -1193,6 +1198,7 @@ public enum Pointcut {
// Return type
void.class,
// Params
+ "java.util.concurrent.atomic.AtomicInteger",
"org.hl7.fhir.instance.model.api.IIdType",
"org.hl7.fhir.instance.model.api.IBaseResource",
"ca.uhn.fhir.rest.api.server.RequestDetails",
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/binstore/BinaryStorageInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BinaryStorageInterceptor.java
index 39367114555..9fa3ff6b5c2 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BinaryStorageInterceptor.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BinaryStorageInterceptor.java
@@ -32,6 +32,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Interceptor
@@ -43,7 +44,7 @@ public class BinaryStorageInterceptor {
private FhirContext myCtx;
@Hook(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE)
- public void expungeResource(IBaseResource theResource) {
+ public void expungeResource(AtomicInteger theCounter, IBaseResource theResource) {
Class extends IBase> binaryType = myCtx.getElementDefinition("base64Binary").getImplementingClass();
List extends IBase> binaryElements = myCtx.newTerser().getAllPopulatedChildElementsOfType(theResource, binaryType);
@@ -57,6 +58,7 @@ public class BinaryStorageInterceptor {
for (String next : attachmentIds) {
myBinaryStorageSvc.expungeBlob(theResource.getIdElement(), next);
+ theCounter.incrementAndGet();
}
}
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 92%
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 0f5485b864d..f092b6d73e4 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,6 +20,7 @@ package ca.uhn.fhir.jpa.dao.expunge;
* #L%
*/
+import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.jpa.util.ExpungeOutcome;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@@ -29,22 +30,21 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Component;
-import org.springframework.transaction.PlatformTransactionManager;
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 +53,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;
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 4cf51db40ca..051e171704b 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
@@ -139,27 +139,36 @@ class ResourceExpungeService implements IResourceExpungeService {
}
}
- private void expungeHistoricalVersion(RequestDetails theRequestDetails, Long theNextVersionId) {
+ private void expungeHistoricalVersion(RequestDetails theRequestDetails, Long theNextVersionId, AtomicInteger theRemainingCount) {
ResourceHistoryTable version = myResourceHistoryTableDao.findById(theNextVersionId).orElseThrow(IllegalArgumentException::new);
IdDt id = version.getIdDt();
ourLog.info("Deleting resource version {}", id.getValue());
- // Interceptor call: STORAGE_PRESTORAGE_EXPUNGE_RESOURCE
+ callHooks(theRequestDetails, theRemainingCount, version, id);
+
+ myResourceHistoryTagDao.deleteAll(version.getTags());
+ myResourceHistoryTableDao.delete(version);
+
+ theRemainingCount.decrementAndGet();
+ }
+
+ private void callHooks(RequestDetails theRequestDetails, AtomicInteger theRemainingCount, ResourceHistoryTable theVersion, IdDt theId) {
+ final AtomicInteger counter = new AtomicInteger();
if (JpaInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, myInterceptorBroadcaster, theRequestDetails)) {
- IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(id.getResourceType());
- IBaseResource resource = resourceDao.toResource(version, false);
+ IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theId.getResourceType());
+ IBaseResource resource = resourceDao.toResource(theVersion, false);
HookParams params = new HookParams()
- .add(IIdType.class, id)
+ .add(AtomicInteger.class, counter)
+ .add(IIdType.class, theId)
.add(IBaseResource.class, resource)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, params);
}
-
- myResourceHistoryTagDao.deleteAll(version.getTags());
- myResourceHistoryTableDao.delete(version);
+ theRemainingCount.addAndGet(-1 * counter.get());
}
+
@Override
@Transactional
public void expungeHistoricalVersionsOfIds(RequestDetails theRequestDetails, List theResourceIds, AtomicInteger theRemainingCount) {
@@ -175,19 +184,19 @@ class ResourceExpungeService implements IResourceExpungeService {
@Transactional
public void expungeHistoricalVersions(RequestDetails theRequestDetails, List theHistoricalIds, AtomicInteger theRemainingCount) {
for (Long next : theHistoricalIds) {
- expungeHistoricalVersion(theRequestDetails, next);
- if (theRemainingCount.decrementAndGet() <= 0) {
+ expungeHistoricalVersion(theRequestDetails, next, theRemainingCount);
+ if (theRemainingCount.get() <= 0) {
return;
}
}
}
- private void expungeCurrentVersionOfResource(RequestDetails theRequestDetails, Long myResourceId, AtomicInteger theRemainingCount) {
- ResourceTable resource = myResourceTableDao.findById(myResourceId).orElseThrow(IllegalStateException::new);
+ private void expungeCurrentVersionOfResource(RequestDetails theRequestDetails, Long theResourceId, AtomicInteger theRemainingCount) {
+ ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalStateException::new);
ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersion(resource.getId(), resource.getVersion());
if (currentVersion != null) {
- expungeHistoricalVersion(theRequestDetails, currentVersion.getId());
+ expungeHistoricalVersion(theRequestDetails, currentVersion.getId(), theRemainingCount);
}
ourLog.info("Expunging current version of resource {}", resource.getIdDt().getValue());
@@ -203,8 +212,6 @@ class ResourceExpungeService implements IResourceExpungeService {
}
myResourceTableDao.delete(resource);
-
- theRemainingCount.decrementAndGet();
}
@Override
@@ -230,8 +237,8 @@ class ResourceExpungeService implements IResourceExpungeService {
Slice versionIds = myResourceHistoryTableDao.findForResourceId(page, resource.getId(), resource.getVersion());
ourLog.debug("Found {} versions of resource {} to expunge", versionIds.getNumberOfElements(), resource.getIdDt().getValue());
for (Long nextVersionId : versionIds) {
- expungeHistoricalVersion(theRequestDetails, nextVersionId);
- if (theRemainingCount.decrementAndGet() <= 0) {
+ expungeHistoricalVersion(theRequestDetails, nextVersionId, theRemainingCount);
+ if (theRemainingCount.get() <= 0) {
return;
}
}
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..a03d5b5ace6
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeHookTest.java
@@ -0,0 +1,93 @@
+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.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.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+
+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();
+
+ assertPatientGone(id);
+ }
+
+ private void assertPatientGone(IIdType theId) {
+ try {
+ myPatientDao.read(theId);
+ 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));
+ myPatientDao.delete(expungeId);
+
+ ExpungeOptions options = new ExpungeOptions();
+ options.setExpungeDeletedResources(true);
+
+ myExpungeResourceLatch.setExpectedCount(2);
+ myExpungeService.expunge("Patient", expungeId.getIdPartAsLong(), null, options, null);
+ HookParams hookParams = myExpungeResourceLatch.awaitExpected().get(0);
+
+ IIdType hookId = hookParams.get(IIdType.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 {
if (object instanceof IBaseResource) {
IBaseResource resource = (IBaseResource) object;
return resource.getClass().getSimpleName() + " { " + resource.getIdElement().getValue() + " }";
- } else {
+ } else if (object != null) {
return object.toString();
+ } else {
+ return "null";
}
}
}