Merge pull request #1393 from jamesagnew/expunge-resource-hook
Expunge resource hook
This commit is contained in:
commit
3460069d38
|
@ -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 <code>true</code> if the interceptor was found and removed
|
||||
*/
|
||||
void unregisterInterceptor(Object theInterceptor);
|
||||
boolean unregisterInterceptor(Object theInterceptor);
|
||||
|
||||
void registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor);
|
||||
|
||||
|
|
|
@ -1166,9 +1166,14 @@ public enum Pointcut {
|
|||
/**
|
||||
* Invoked before a resource is about to be expunged via the <code>$expunge</code> operation.
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* <p>
|
||||
* Hooks may accept the following parameters:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>java.util.concurrent.atomic.AtomicInteger - The counter holding the number of records deleted.</li>
|
||||
* <li>org.hl7.fhir.instance.model.api.IIdType - The ID of the resource that is about to be deleted</li>
|
||||
* <li>org.hl7.fhir.instance.model.api.IBaseResource - The resource that is about to be deleted</li>
|
||||
* <li>
|
||||
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<ExpungeOutcome> {
|
||||
public class ExpungeOperation implements Callable<ExpungeOutcome> {
|
||||
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<ExpungeOutcome> {
|
|||
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;
|
|
@ -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) {
|
||||
|
|
|
@ -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<Long> theResourceIds, AtomicInteger theRemainingCount) {
|
||||
|
@ -175,19 +184,19 @@ class ResourceExpungeService implements IResourceExpungeService {
|
|||
@Transactional
|
||||
public void expungeHistoricalVersions(RequestDetails theRequestDetails, List<Long> 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<Long> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Patient> 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());
|
||||
}
|
||||
}
|
|
@ -30,8 +30,10 @@ public class FhirObjectPrinter implements Function<Object, String> {
|
|||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue