Ensure that JPA PRESHOW interceptor broadcasts don't include expunged

resources
This commit is contained in:
James Agnew 2019-10-03 15:48:34 -04:00
parent 38ad11be64
commit 836fac9a30
6 changed files with 184 additions and 27 deletions

View File

@ -31,6 +31,7 @@ import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource; import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.util.InterceptorUtil;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.*; import ca.uhn.fhir.rest.api.server.*;
@ -52,7 +53,6 @@ import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
public class PersistedJpaBundleProvider implements IBundleProvider { public class PersistedJpaBundleProvider implements IBundleProvider {
@ -319,24 +319,8 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
// Execute the query and make sure we return distinct results // Execute the query and make sure we return distinct results
List<IBaseResource> resources = new ArrayList<>(); List<IBaseResource> resources = new ArrayList<>();
theSearchBuilder.loadResourcesByPid(thePids, includedPidList, resources, false, myRequest); theSearchBuilder.loadResourcesByPid(thePids, includedPidList, resources, false, myRequest);
// resources.removeIf(t->t == null); aaaaaaaaabbbbbbbb
// Interceptor call: STORAGE_PRESHOW_RESOURCE InterceptorUtil.fireStoragePreshowResource(resources, myRequest, myInterceptorBroadcaster);
// This can be used to remove results from the search result details before
// the user has a chance to know that they were in the results
if (resources.size() > 0) {
SimplePreResourceShowDetails accessDetails = new SimplePreResourceShowDetails(resources);
HookParams params = new HookParams()
.add(IPreResourceShowDetails.class, accessDetails)
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params);
resources = resources
.stream()
.filter(t -> t != null)
.collect(Collectors.toList());
}
return resources; return resources;
} }

View File

@ -34,6 +34,7 @@ import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc; import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.InterceptorUtil;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.CacheControlDirective; import ca.uhn.fhir.rest.api.CacheControlDirective;
@ -485,6 +486,10 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
List<IBaseResource> resources = new ArrayList<>(); List<IBaseResource> resources = new ArrayList<>();
theSb.loadResourcesByPid(pids, includedPidsList, resources, false, theRequestDetails); theSb.loadResourcesByPid(pids, includedPidsList, resources, false, theRequestDetails);
// Hook: STORAGE_PRESHOW_RESOURCES
InterceptorUtil.fireStoragePreshowResource(resources, theRequestDetails, myInterceptorBroadcaster);
return new SimpleBundleProvider(resources); return new SimpleBundleProvider(resources);
}); });
} }

View File

@ -0,0 +1,39 @@
package ca.uhn.fhir.jpa.util;
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.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
public class InterceptorUtil {
/**
* Fires {@link Pointcut#STORAGE_PRESHOW_RESOURCES} interceptor hook, and potentially remove resources
* from the resource list
*/
public static void fireStoragePreshowResource(List<IBaseResource> theResources, RequestDetails theRequest, IInterceptorBroadcaster theInterceptorBroadcaster) {
theResources.removeIf(t -> t == null);
// Interceptor call: STORAGE_PRESHOW_RESOURCE
// This can be used to remove results from the search result details before
// the user has a chance to know that they were in the results
if (theResources.size() > 0) {
SimplePreResourceShowDetails accessDetails = new SimplePreResourceShowDetails(theResources);
HookParams params = new HookParams()
.add(IPreResourceShowDetails.class, accessDetails)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
JpaInterceptorBroadcaster.doCallHooks(theInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params);
theResources.removeIf(t -> t == null);
}
}
}

View File

@ -0,0 +1,134 @@
package ca.uhn.fhir.jpa.dao.r5;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r5.model.IdType;
import org.hl7.fhir.r5.model.Patient;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
@SuppressWarnings({"unchecked", "Duplicates"})
public class StorageInterceptorEventsR5Test extends BaseJpaR5Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(StorageInterceptorEventsR5Test.class);
@Test
public void before() {
}
@Test
public void testPreShowEventsDontIncludeExpungedResources_AsyncSearch() {
when(mySrd.getServer().getPagingProvider()).thenReturn(new DatabaseBackedPagingProvider());
Patient p0 = new Patient();
p0.setId("P0");
p0.addIdentifier().setValue("P0");
myPatientDao.update(p0);
Patient p1 = new Patient();
p1.setId("P1");
p1.addIdentifier().setValue("P1");
myPatientDao.update(p1);
Patient p2 = new Patient();
p2.setId("P2");
p2.addIdentifier().setValue("P2");
myPatientDao.update(p2);
AtomicInteger showedCounter = new AtomicInteger(0);
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESHOW_RESOURCES, (thePointcut, theArgs) -> {
int showedCountThisPass = theArgs.get(IPreResourceShowDetails.class).size();
showedCounter.addAndGet(showedCountThisPass);
});
// Initial search returns all
SearchParameterMap params = new SearchParameterMap();
IBundleProvider search = myPatientDao.search(params, mySrd);
assertTrue(search.getClass().toString(), search instanceof PersistedJpaBundleProvider);
List<IBaseResource> found = search.getResources(0, 100);
assertEquals(3, found.size());
assertEquals(3, showedCounter.get());
// Delete and expunge one
myPatientDao.delete(new IdType("Patient/P1"));
mySystemDao.expunge(new ExpungeOptions().setExpungeDeletedResources(true), mySrd);
showedCounter.set(0);
// Next search should return only the non-expunged ones
params = new SearchParameterMap();
found = myPatientDao.search(params, mySrd).getResources(0, 100);
assertEquals(2, found.size());
assertEquals(2, showedCounter.get());
}
@Test
public void testPreShowEventsDontIncludeExpungedResources_SyncSearch() {
Patient p0 = new Patient();
p0.setId("P0");
p0.addIdentifier().setValue("P0");
myPatientDao.update(p0);
Patient p1 = new Patient();
p1.setId("P1");
p1.addIdentifier().setValue("P1");
myPatientDao.update(p1);
Patient p2 = new Patient();
p2.setId("P2");
p2.addIdentifier().setValue("P2");
myPatientDao.update(p2);
AtomicInteger showedCounter = new AtomicInteger(0);
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESHOW_RESOURCES, (thePointcut, theArgs) -> {
int showedCountThisPass = theArgs.get(IPreResourceShowDetails.class).size();
showedCounter.addAndGet(showedCountThisPass);
});
// Initial search returns all
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronous(true);
IBundleProvider search = myPatientDao.search(params, mySrd);
assertTrue(search.getClass().toString(), search instanceof SimpleBundleProvider);
List<IBaseResource> found = search.getResources(0, 100);
assertEquals(3, found.size());
assertEquals(3, showedCounter.get());
// Delete and expunge one
myPatientDao.delete(new IdType("Patient/P1"));
mySystemDao.expunge(new ExpungeOptions().setExpungeDeletedResources(true), mySrd);
showedCounter.set(0);
// Next search should return only the non-expunged ones
params = new SearchParameterMap();
found = myPatientDao.search(params, mySrd).getResources(0, 100);
assertEquals(2, found.size());
assertEquals(2, showedCounter.get());
}
@After
public void after() {
myInterceptorRegistry.unregisterAllInterceptors();
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -59,7 +59,6 @@ public class SearchParameterMap implements Serializable {
private SortSpec mySort; private SortSpec mySort;
private SummaryEnum mySummaryMode; private SummaryEnum mySummaryMode;
private SearchTotalModeEnum mySearchTotalMode; private SearchTotalModeEnum mySearchTotalMode;
private Long myQueryCacheExpiryTime;
/** /**
* Constructor * Constructor
@ -75,14 +74,6 @@ public class SearchParameterMap implements Serializable {
add(theName, theParam); add(theName, theParam);
} }
public Long getQueryCacheExpiryTime() {
return myQueryCacheExpiryTime;
}
public void setQueryCacheExpiryTime(Long theQueryCacheExpiryTime) {
myQueryCacheExpiryTime = theQueryCacheExpiryTime;
}
public SummaryEnum getSummaryMode() { public SummaryEnum getSummaryMode() {
return mySummaryMode; return mySummaryMode;
} }

View File

@ -322,6 +322,10 @@
The subscription triggering operation was not able to handle commas within search URLs being The subscription triggering operation was not able to handle commas within search URLs being
used to trigger resources for subscription checking. This has been corrected. used to trigger resources for subscription checking. This has been corrected.
</action> </action>
<action type="fix">
In some cases where resources were recently expunged, null entries could be passed to JPA interceptors registered
against the STORAGE_PRESHOW_RESOURCES hook.
</action>
</release> </release>
<release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)"> <release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)">
<action type="fix"> <action type="fix">