Merge pull request #1393 from jamesagnew/expunge-resource-hook

Expunge resource hook
This commit is contained in:
James Agnew 2019-07-25 05:41:39 -04:00 committed by GitHub
commit 3460069d38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 144 additions and 32 deletions

View File

@ -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);

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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";
}
}
}