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: + * + *

+ * 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 { 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"; } } }