From fe21dba4a69942e21aeafbd5216f072280e47bcd Mon Sep 17 00:00:00 2001
From: Ken Stevens
Date: Wed, 17 Jul 2019 13:26:50 -0400
Subject: [PATCH 1/9] Added Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE.
Also added a couple of tests.
---
.../interceptor/api/IInterceptorService.java | 3 +-
.../ca/uhn/fhir/interceptor/api/Pointcut.java | 39 ++++++++
.../executor/InterceptorService.java | 9 +-
...{ExpungeRun.java => ExpungeOperation.java} | 48 +++++++++-
.../fhir/jpa/dao/expunge/ExpungeService.java | 6 +-
.../dao/expunge/ResourceExpungeService.java | 4 +-
.../fhir/jpa/dao/expunge/ExpungeHookTest.java | 89 +++++++++++++++++++
.../model/concurrency/FhirObjectPrinter.java | 4 +-
8 files changed, 187 insertions(+), 15 deletions(-)
rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/{ExpungeRun.java => ExpungeOperation.java} (75%)
create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeHookTest.java
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
*/
- STORAGE_PRESTORAGE_EXPUNGE_RESOURCE(
+ STORAGE_PRESTORAGE_EXPUNGE_RESOURCE (
// 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",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
From 64e83fa3dfcfcf3393812a76f9d489b9984ce386 Mon Sep 17 00:00:00 2001
From: Ken Stevens
Date: Wed, 24 Jul 2019 13:00:49 -0400
Subject: [PATCH 6/9] Revert "Recovering Pointcut from a failed merge"
This reverts commit 19c0b4fd
---
.../java/ca/uhn/fhir/interceptor/api/Pointcut.java | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
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 d3b6843a960..fcac3de9935 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,15 +1166,11 @@ 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:
*
*
*
org.hl7.fhir.instance.model.api.IIdType - The ID of the resource that is about to be deleted
- *
java.util.concurrent.atomic.AtomicInteger - The counter holding the number of records deleted.
+ *
org.hl7.fhir.instance.model.api.IBaseResource - The resource that is about to be deleted
*
* 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
@@ -1193,12 +1189,12 @@ public enum Pointcut {
* Hooks should return void.
*
*/
- STORAGE_PRESTORAGE_EXPUNGE_RESOURCE (
+ STORAGE_PRESTORAGE_EXPUNGE_RESOURCE(
// 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",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
),
From e05387da022634dda10c9f8d051c7fb5372dedc0 Mon Sep 17 00:00:00 2001
From: Ken Stevens
Date: Wed, 24 Jul 2019 13:46:20 -0400
Subject: [PATCH 7/9] Recover from failed merge again. This time with feeling!
---
.../ca/uhn/fhir/interceptor/api/Pointcut.java | 6 +++
.../binstore/BinaryStorageInterceptor.java | 4 +-
.../jpa/dao/expunge/ExpungeOperation.java | 26 -------------
.../dao/expunge/ResourceExpungeService.java | 37 +++++++++++--------
.../fhir/jpa/dao/expunge/ExpungeHookTest.java | 8 ++--
5 files changed, 36 insertions(+), 45 deletions(-)
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