Added Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE.

Also added a couple of tests.
This commit is contained in:
Ken Stevens 2019-07-17 13:26:50 -04:00
parent 160b221f5f
commit fe21dba4a6
8 changed files with 187 additions and 15 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. * Unregister an interceptor. This method has no effect if the given interceptor is not already registered.
* *
* @param theInterceptor The interceptor to unregister * @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); void registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor);

View File

@ -1200,6 +1200,45 @@ public enum Pointcut {
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
), ),
/**
* Invoked before expunge is called on a resource.
* <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>
* Hooks may accept the following parameters:
* <ul>
* <li>java.util.concurrent.atomic.AtomicInteger - The counter holding the number of records deleted.</li>
* <li>ca.uhn.fhir.model.primitive.IdDt - The id of the resource to be deleted. Note that version may be null.</li>
* <li>
* 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.
* </li>
* <li>
* 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.
* </li>
* </ul>
* <p>
* Hooks should return void.
* </p>
*/
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 * Note that this is a performance tracing hook. Use with caution in production
* systems, since calling it may (or may not) carry a cost. * systems, since calling it may (or may not) carry a cost.

View File

@ -208,11 +208,12 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
} }
@Override @Override
public void unregisterInterceptor(Object theInterceptor) { public boolean unregisterInterceptor(Object theInterceptor) {
synchronized (myRegistryMutex) { synchronized (myRegistryMutex) {
myInterceptors.removeIf(t -> t == theInterceptor); boolean removed = myInterceptors.removeIf(t -> t == theInterceptor);
myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); removed |= myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); removed |= myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
return removed;
} }
} }

View File

@ -20,9 +20,15 @@ package ca.uhn.fhir.jpa.dao.expunge;
* #L% * #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.ExpungeOptions;
import ca.uhn.fhir.jpa.util.ExpungeOutcome; 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.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; 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.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.Id;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@Component @Component
@Scope("prototype") @Scope("prototype")
public class ExpungeRun implements Callable<ExpungeOutcome> { public class ExpungeOperation implements Callable<ExpungeOutcome> {
private static final Logger ourLog = LoggerFactory.getLogger(ExpungeService.class); private static final Logger ourLog = LoggerFactory.getLogger(ExpungeService.class);
@Autowired
private PlatformTransactionManager myPlatformTransactionManager;
@Autowired @Autowired
private IResourceExpungeService myExpungeDaoService; private IResourceExpungeService myExpungeDaoService;
@Autowired @Autowired
private PartitionRunner myPartitionRunner; private PartitionRunner myPartitionRunner;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
private final String myResourceName; private final String myResourceName;
private final Long myResourceId; private final Long myResourceId;
@ -53,7 +60,7 @@ public class ExpungeRun implements Callable<ExpungeOutcome> {
private final RequestDetails myRequestDetails; private final RequestDetails myRequestDetails;
private final AtomicInteger myRemainingCount; 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; myResourceName = theResourceName;
myResourceId = theResourceId; myResourceId = theResourceId;
myVersion = theVersion; myVersion = theVersion;
@ -64,6 +71,15 @@ public class ExpungeRun implements Callable<ExpungeOutcome> {
@Override @Override
public ExpungeOutcome call() { public ExpungeOutcome call() {
final IdDt id;
callHooks();
if (expungeLimitReached()) {
return expungeOutcome();
}
if (myExpungeOptions.isExpungeDeletedResources() && myVersion == null) { if (myExpungeOptions.isExpungeDeletedResources() && myVersion == null) {
expungeDeletedResources(); expungeDeletedResources();
if (expungeLimitReached()) { if (expungeLimitReached()) {
@ -81,6 +97,30 @@ public class ExpungeRun implements Callable<ExpungeOutcome> {
return expungeOutcome(); 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() { private void expungeDeletedResources() {
Slice<Long> resourceIds = findHistoricalVersionsOfDeletedResources(); Slice<Long> resourceIds = findHistoricalVersionsOfDeletedResources();

View File

@ -44,7 +44,7 @@ public abstract class ExpungeService {
private IResourceExpungeService myExpungeDaoService; private IResourceExpungeService myExpungeDaoService;
@Lookup @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) { public ExpungeOutcome expunge(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions, RequestDetails theRequest) {
ourLog.info("Expunge: ResourceName[{}] Id[{}] Version[{}] Options[{}]", theResourceName, theResourceId, theVersion, theExpungeOptions); 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); ExpungeOperation expungeOperation = getExpungeOperation(theResourceName, theResourceId, theVersion, theExpungeOptions, theRequest);
return expungeRun.call(); return expungeOperation.call();
} }
public void deleteAllSearchParams(Long theResourceId) { public void deleteAllSearchParams(Long theResourceId) {

View File

@ -154,8 +154,8 @@ class ResourceExpungeService implements IResourceExpungeService {
} }
} }
private void expungeCurrentVersionOfResource(Long myResourceId, AtomicInteger theRemainingCount) { private void expungeCurrentVersionOfResource(Long theResourceId, AtomicInteger theRemainingCount) {
ResourceTable resource = myResourceTableDao.findById(myResourceId).orElseThrow(IllegalStateException::new); ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalStateException::new);
ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersion(resource.getId(), resource.getVersion()); ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersion(resource.getId(), resource.getVersion());
if (currentVersion != null) { if (currentVersion != null) {

View File

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

View File

@ -30,8 +30,10 @@ public class FhirObjectPrinter implements Function<Object, String> {
if (object instanceof IBaseResource) { if (object instanceof IBaseResource) {
IBaseResource resource = (IBaseResource) object; IBaseResource resource = (IBaseResource) object;
return resource.getClass().getSimpleName() + " { " + resource.getIdElement().getValue() + " }"; return resource.getClass().getSimpleName() + " { " + resource.getIdElement().getValue() + " }";
} else { } else if (object != null) {
return object.toString(); return object.toString();
} else {
return "null";
} }
} }
} }