Rewrite deferred interceptor callback framework (#2121)

* Rewrite deferred interceptor callback framework

* Improve deferred handling

* Null guard

* Test fix
This commit is contained in:
James Agnew 2020-10-07 20:08:18 -04:00 committed by GitHub
parent ebbdafb107
commit d2aae361bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 136 additions and 140 deletions

View File

@ -46,6 +46,7 @@ import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
@ -81,7 +82,7 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
* won't be indexed and searches won't work. * won't be indexed and searches won't work.
* @param theRequestDetails TODO * @param theRequestDetails TODO
*/ */
DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequestDetails); DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, @Nonnull TransactionDetails theTransactionDetails, RequestDetails theRequestDetails);
DaoMethodOutcome create(T theResource, String theIfNoneExist, RequestDetails theRequestDetails); DaoMethodOutcome create(T theResource, String theIfNoneExist, RequestDetails theRequestDetails);
@ -95,7 +96,7 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
* This method does not throw an exception if there are delete conflicts, but populates them * This method does not throw an exception if there are delete conflicts, but populates them
* in the provided list * in the provided list
*/ */
DaoMethodOutcome delete(IIdType theResource, DeleteConflictList theDeleteConflictsListToPopulate, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails); DaoMethodOutcome delete(IIdType theResource, DeleteConflictList theDeleteConflictsListToPopulate, RequestDetails theRequestDetails, @Nonnull TransactionDetails theTransactionDetails);
/** /**
* This method throws an exception if there are delete conflicts * This method throws an exception if there are delete conflicts
@ -167,7 +168,6 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
T readByPid(ResourcePersistentId thePid); T readByPid(ResourcePersistentId thePid);
/** /**
* @param theId
* @param theRequestDetails TODO * @param theRequestDetails TODO
* @throws ResourceNotFoundException If the ID is not known to the server * @throws ResourceNotFoundException If the ID is not known to the server
*/ */
@ -244,7 +244,7 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
* @param theForceUpdateVersion Create a new version with the same contents as the current version even if the content hasn't changed (this is mostly useful for * @param theForceUpdateVersion Create a new version with the same contents as the current version even if the content hasn't changed (this is mostly useful for
* resources mapping to external content such as external code systems) * resources mapping to external content such as external code systems)
*/ */
DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails); DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequestDetails, @Nonnull TransactionDetails theTransactionDetails);
/** /**
* Not supported in DSTU1! * Not supported in DSTU1!

View File

@ -113,8 +113,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
@ -1258,7 +1256,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
.add(RequestDetails.class, theRequestDetails) .add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails) .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(TransactionDetails.class, theTransactionDetails); .add(TransactionDetails.class, theTransactionDetails);
doCallHooks(theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, hookParams); doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, hookParams);
// Perform update // Perform update
ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, entity, null, thePerformIndexing, thePerformIndexing, theTransactionDetails, theForceUpdateVersion, thePerformIndexing); ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, entity, null, thePerformIndexing, thePerformIndexing, theTransactionDetails, theForceUpdateVersion, thePerformIndexing);
@ -1283,18 +1281,13 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
// Notify interceptors // Notify interceptors
if (!savedEntity.isUnchangedInCurrentOperation()) { if (!savedEntity.isUnchangedInCurrentOperation()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { hookParams = new HookParams()
@Override .add(IBaseResource.class, theOldResource)
public void beforeCommit(boolean readOnly) { .add(IBaseResource.class, theResource)
HookParams hookParams = new HookParams() .add(RequestDetails.class, theRequestDetails)
.add(IBaseResource.class, theOldResource) .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(IBaseResource.class, theResource) .add(TransactionDetails.class, theTransactionDetails);
.add(RequestDetails.class, theRequestDetails) doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, hookParams);
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(TransactionDetails.class, theTransactionDetails);
doCallHooks(theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, hookParams);
}
});
} }
return savedEntity; return savedEntity;
@ -1461,8 +1454,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
StringBuilder retVal = new StringBuilder(); StringBuilder retVal = new StringBuilder();
List<IPrimitiveType<String>> childElements = theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, stringType); List<IPrimitiveType<String>> childElements = theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, stringType);
for (@SuppressWarnings("rawtypes") for (IPrimitiveType<String> nextType : childElements) {
IPrimitiveType<String> nextType : childElements) {
if (stringType.equals(nextType.getClass())) { if (stringType.equals(nextType.getClass())) {
String nextValue = nextType.getValueAsString(); String nextValue = nextType.getValueAsString();
if (isNotBlank(nextValue)) { if (isNotBlank(nextValue)) {

View File

@ -207,7 +207,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
@Override @Override
public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) { public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, @Nonnull TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) {
return myTransactionService.execute(theRequestDetails, tx -> doCreateForPost(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails)); return myTransactionService.execute(theRequestDetails, tx -> doCreateForPost(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails));
} }
@ -305,7 +305,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
.add(RequestDetails.class, theRequest) .add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(TransactionDetails.class, theTransactionDetails); .add(TransactionDetails.class, theTransactionDetails);
doCallHooks(theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams); doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams);
// Perform actual DB update // Perform actual DB update
ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theTransactionDetails, false, thePerformIndexing); ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theTransactionDetails, false, thePerformIndexing);
@ -343,17 +343,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// Notify JPA interceptors // Notify JPA interceptors
if (!updatedEntity.isUnchangedInCurrentOperation()) { if (!updatedEntity.isUnchangedInCurrentOperation()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { hookParams = new HookParams()
@Override .add(IBaseResource.class, theResource)
public void beforeCommit(boolean readOnly) { .add(RequestDetails.class, theRequest)
HookParams hookParams = new HookParams() .addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(IBaseResource.class, theResource) .add(TransactionDetails.class, theTransactionDetails);
.add(RequestDetails.class, theRequest) doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, hookParams);
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(TransactionDetails.class, theTransactionDetails);
doCallHooks(theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, hookParams);
}
});
} }
DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, theResource).setCreated(true); DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, theResource).setCreated(true);
@ -400,7 +395,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
@Override @Override
public DaoMethodOutcome delete(IIdType theId, DeleteConflictList theDeleteConflicts, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { public DaoMethodOutcome delete(IIdType theId, DeleteConflictList theDeleteConflicts, RequestDetails theRequestDetails, @Nonnull TransactionDetails theTransactionDetails) {
validateIdPresentForDelete(theId); validateIdPresentForDelete(theId);
validateDeleteEnabled(); validateDeleteEnabled();
@ -439,7 +434,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
.add(RequestDetails.class, theRequestDetails) .add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails) .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(TransactionDetails.class, theTransactionDetails); .add(TransactionDetails.class, theTransactionDetails);
doCallHooks(theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hook); doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hook);
myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequestDetails, theTransactionDetails); myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequestDetails, theTransactionDetails);
@ -455,17 +450,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
resourceToDelete.setId(entity.getIdDt()); resourceToDelete.setId(entity.getIdDt());
// Notify JPA interceptors // Notify JPA interceptors
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { HookParams hookParams = new HookParams()
@Override .add(IBaseResource.class, resourceToDelete)
public void beforeCommit(boolean readOnly) { .add(RequestDetails.class, theRequestDetails)
HookParams hookParams = new HookParams() .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(IBaseResource.class, resourceToDelete) .add(TransactionDetails.class, theTransactionDetails);
.add(RequestDetails.class, theRequestDetails) if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts()) {
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails) theTransactionDetails.addDeferredInterceptorBroadcast(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
.add(TransactionDetails.class, theTransactionDetails); } else {
doCallHooks(theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams); doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
} }
});
DaoMethodOutcome outcome = toMethodOutcome(theRequestDetails, savedEntity, resourceToDelete).setCreated(true); DaoMethodOutcome outcome = toMethodOutcome(theRequestDetails, savedEntity, resourceToDelete).setCreated(true);
@ -516,7 +510,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@NotNull @NotNull
@Override @Override
public DeleteMethodOutcome deletePidList(String theUrl, Collection<ResourcePersistentId> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theTheRequest) { public DeleteMethodOutcome deletePidList(String theUrl, Collection<ResourcePersistentId> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequest) {
StopWatch w = new StopWatch(); StopWatch w = new StopWatch();
TransactionDetails transactionDetails = new TransactionDetails(); TransactionDetails transactionDetails = new TransactionDetails();
List<ResourceTable> deletedResources = new ArrayList<>(); List<ResourceTable> deletedResources = new ArrayList<>();
@ -529,23 +523,23 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// Notify IServerOperationInterceptors about pre-action call // Notify IServerOperationInterceptors about pre-action call
HookParams hooks = new HookParams() HookParams hooks = new HookParams()
.add(IBaseResource.class, resourceToDelete) .add(IBaseResource.class, resourceToDelete)
.add(RequestDetails.class, theTheRequest) .add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theTheRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(TransactionDetails.class, transactionDetails); .add(TransactionDetails.class, transactionDetails);
doCallHooks(theTheRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks); doCallHooks(transactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks);
myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theTheRequest, transactionDetails); myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequest, transactionDetails);
// Notify interceptors // Notify interceptors
IdDt idToDelete = entity.getIdDt(); IdDt idToDelete = entity.getIdDt();
if (theTheRequest != null) { if (theRequest != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theTheRequest, idToDelete.getResourceType(), idToDelete); ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, idToDelete.getResourceType(), idToDelete);
notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails);
} }
// Perform delete // Perform delete
updateEntityForDelete(theTheRequest, transactionDetails, entity); updateEntityForDelete(theRequest, transactionDetails, entity);
resourceToDelete.setId(entity.getIdDt()); resourceToDelete.setId(entity.getIdDt());
// Notify JPA interceptors // Notify JPA interceptors
@ -554,10 +548,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public void beforeCommit(boolean readOnly) { public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams() HookParams hookParams = new HookParams()
.add(IBaseResource.class, resourceToDelete) .add(IBaseResource.class, resourceToDelete)
.add(RequestDetails.class, theTheRequest) .add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theTheRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(TransactionDetails.class, transactionDetails); .add(TransactionDetails.class, transactionDetails);
doCallHooks(theTheRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams); doCallHooks(transactionDetails, theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
} }
}); });
} }
@ -1359,7 +1353,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
@Override @Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, TransactionDetails theTransactionDetails) { public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, @Nonnull TransactionDetails theTransactionDetails) {
if (theResource == null) { if (theResource == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "missingBody"); String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "missingBody");
throw new InvalidRequestException(msg); throw new InvalidRequestException(msg);

View File

@ -40,6 +40,7 @@ import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails; import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.param.QualifierDetails; import ca.uhn.fhir.rest.param.QualifierDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -184,8 +185,12 @@ public abstract class BaseStorageDao {
return outcome; return outcome;
} }
protected void doCallHooks(RequestDetails theRequestDetails, Pointcut thePointcut, HookParams theParams) { protected void doCallHooks(TransactionDetails theTransactionDetails, RequestDetails theRequestDetails, Pointcut thePointcut, HookParams theParams) {
JpaInterceptorBroadcaster.doCallHooks(getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams); if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts(thePointcut)) {
theTransactionDetails.addDeferredInterceptorBroadcast(thePointcut, theParams);
} else {
JpaInterceptorBroadcaster.doCallHooks(getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams);
}
} }
protected abstract IInterceptorBroadcaster getInterceptorBroadcaster(); protected abstract IInterceptorBroadcaster getInterceptorBroadcaster();

View File

@ -68,6 +68,7 @@ import ca.uhn.fhir.util.ResourceReferenceInfo;
import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.UrlUtil;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
@ -540,9 +541,11 @@ public abstract class BaseTransactionProcessor {
private Map<IBase, IBasePersistedResource> doTransactionWriteOperations(final ServletRequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set<IIdType> theAllIds, private Map<IBase, IBasePersistedResource> doTransactionWriteOperations(final ServletRequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder, List<IBase> theEntries, StopWatch theTransactionStopWatch) { Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder, List<IBase> theEntries, StopWatch theTransactionStopWatch) {
if (theRequest != null) { theTransactionDetails.beginAcceptingDeferredInterceptorBroadcasts(
theRequest.startDeferredOperationCallback(); Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED,
} Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED,
Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED
);
try { try {
Set<String> deletedResources = new HashSet<>(); Set<String> deletedResources = new HashSet<>();
@ -980,11 +983,19 @@ public abstract class BaseTransactionProcessor {
} }
ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement);
} }
ListMultimap<Pointcut, HookParams> deferredBroadcastEvents = theTransactionDetails.endAcceptingDeferredInterceptorBroadcasts();
for (Map.Entry<Pointcut, HookParams> nextEntry : deferredBroadcastEvents.entries()) {
Pointcut nextPointcut = nextEntry.getKey();
HookParams nextParams = nextEntry.getValue();
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, nextPointcut, nextParams);
}
return entriesToProcess; return entriesToProcess;
} finally { } finally {
if (theRequest != null) { if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts()) {
theRequest.stopDeferredRequestOperationCallbackAndRunDeferredItems(); theTransactionDetails.endAcceptingDeferredInterceptorBroadcasts();
} }
} }
} }

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.api.model.DeleteConflict;
import ca.uhn.fhir.jpa.api.model.DeleteConflictList; import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Condition; import org.hl7.fhir.r4.model.Condition;
@ -243,11 +244,12 @@ public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
} }
private DeleteConflictOutcome deleteConflictsFixedRetryCount(DeleteConflictList theList) { private DeleteConflictOutcome deleteConflictsFixedRetryCount(DeleteConflictList theList) {
TransactionDetails transactionDetails = new TransactionDetails();
for (DeleteConflict next : theList) { for (DeleteConflict next : theList) {
IdDt source = next.getSourceId(); IdDt source = next.getSourceId();
if ("Patient".equals(source.getResourceType())) { if ("Patient".equals(source.getResourceType())) {
ourLog.info("Deleting {}", source); ourLog.info("Deleting {}", source);
myPatientDao.delete(source, theList, null, null); myPatientDao.delete(source, theList, null, transactionDetails);
++myInterceptorDeleteCount; ++myInterceptorDeleteCount;
} }
} }

View File

@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.dao.expunge.ExpungeService; import ca.uhn.fhir.jpa.dao.expunge.ExpungeService;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -95,10 +96,11 @@ public class EmpiPersonDeletingSvc {
private void deleteConflictBatch(DeleteConflictList theDcl, IFhirResourceDao<IBaseResource> theDao) { private void deleteConflictBatch(DeleteConflictList theDcl, IFhirResourceDao<IBaseResource> theDao) {
DeleteConflictList newBatch = new DeleteConflictList(); DeleteConflictList newBatch = new DeleteConflictList();
TransactionDetails transactionDetails = new TransactionDetails();
for (DeleteConflict next : theDcl) { for (DeleteConflict next : theDcl) {
IdDt nextSource = next.getSourceId(); IdDt nextSource = next.getSourceId();
ourLog.info("Have delete conflict {} - Cascading delete", nextSource); ourLog.info("Have delete conflict {} - Cascading delete", nextSource);
theDao.delete(nextSource.toVersionless(), newBatch, null, null); theDao.delete(nextSource.toVersionless(), newBatch, null, transactionDetails);
} }
theDcl.removeAll(); theDcl.removeAll();
theDcl.addAll(newBatch); theDcl.addAll(newBatch);

View File

@ -1,9 +1,7 @@
package ca.uhn.fhir.rest.api.server; package ca.uhn.fhir.rest.api.server;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
@ -15,7 +13,6 @@ import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
@ -64,7 +61,6 @@ public abstract class RequestDetails {
private String myOperation; private String myOperation;
private Map<String, String[]> myParameters; private Map<String, String[]> myParameters;
private byte[] myRequestContents; private byte[] myRequestContents;
private DeferredOperationCallback myDeferredInterceptorBroadcaster;
private String myRequestPath; private String myRequestPath;
private RequestTypeEnum myRequestType; private RequestTypeEnum myRequestType;
private String myResourceName; private String myResourceName;
@ -305,9 +301,6 @@ public abstract class RequestDetails {
* all interceptors * all interceptors
*/ */
public IInterceptorBroadcaster getInterceptorBroadcaster() { public IInterceptorBroadcaster getInterceptorBroadcaster() {
if (myDeferredInterceptorBroadcaster != null) {
return myDeferredInterceptorBroadcaster;
}
return myInterceptorBroadcaster; return myInterceptorBroadcaster;
} }
@ -500,26 +493,6 @@ public abstract class RequestDetails {
myRequestContents = theRequestContents; myRequestContents = theRequestContents;
} }
/**
* Sets the {@link #getInterceptorBroadcaster()} () interceptor broadcaster} handler in
* deferred mode, meaning that any notifications will be queued up for delivery, but
* won't be delivered until {@link #stopDeferredRequestOperationCallbackAndRunDeferredItems()}
* is called.
*/
public void startDeferredOperationCallback() {
myDeferredInterceptorBroadcaster = new DeferredOperationCallback(myInterceptorBroadcaster);
}
/**
* @see #startDeferredOperationCallback()
*/
public void stopDeferredRequestOperationCallbackAndRunDeferredItems() {
DeferredOperationCallback deferredCallback = myDeferredInterceptorBroadcaster;
deferredCallback.playDeferredActions();
myInterceptorBroadcaster = deferredCallback.getWrap();
myDeferredInterceptorBroadcaster = null;
}
public String getTransactionGuid() { public String getTransactionGuid() {
return myTransactionGuid; return myTransactionGuid;
} }
@ -529,47 +502,4 @@ public abstract class RequestDetails {
} }
private class DeferredOperationCallback implements IInterceptorBroadcaster {
private final IInterceptorBroadcaster myWrap;
private final List<Runnable> myDeferredTasks = new ArrayList<>();
private DeferredOperationCallback(@Nonnull IInterceptorBroadcaster theWrap) {
Validate.notNull(theWrap);
myWrap = theWrap;
}
void playDeferredActions() {
myDeferredTasks.forEach(Runnable::run);
}
IInterceptorBroadcaster getWrap() {
return myWrap;
}
@Override
public boolean callHooks(Pointcut thePointcut, HookParams theParams) {
myDeferredTasks.add(() -> myWrap.callHooks(thePointcut, theParams));
return true;
}
@Override
public Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams) {
if (!thePointcut.getReturnType().equals(void.class)) {
return myWrap.callHooksAndReturnObject(thePointcut, theParams);
}
myDeferredTasks.add(() -> myWrap.callHooksAndReturnObject(thePointcut, theParams));
return null;
}
@Override
public boolean hasHooks(Pointcut thePointcut) {
return myWrap.hasHooks(thePointcut);
}
}
} }

View File

@ -20,10 +20,16 @@ package ca.uhn.fhir.rest.api.server.storage;
* #L% * #L%
*/ */
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Pointcut;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -34,7 +40,7 @@ import java.util.function.Supplier;
* (i.e. a FHIR create, read, transaction, etc.). * (i.e. a FHIR create, read, transaction, etc.).
* <p> * <p>
* The intent with this class is to hold things we want to pass from operation to operation within a transaction in * The intent with this class is to hold things we want to pass from operation to operation within a transaction in
* order to avoid looking things up multime times, etc. * order to avoid looking things up multiple times, etc.
* </p> * </p>
* *
* @since 5.0.0 * @since 5.0.0
@ -44,12 +50,14 @@ public class TransactionDetails {
private final Date myTransactionDate; private final Date myTransactionDate;
private Map<IIdType, ResourcePersistentId> myResolvedResourceIds = Collections.emptyMap(); private Map<IIdType, ResourcePersistentId> myResolvedResourceIds = Collections.emptyMap();
private Map<String, Object> myUserData; private Map<String, Object> myUserData;
private ListMultimap<Pointcut, HookParams> myDeferredInterceptorBroadcasts;
private EnumSet<Pointcut> myDeferredInterceptorBroadcastPointcuts;
/** /**
* Constructor * Constructor
*/ */
public TransactionDetails() { public TransactionDetails() {
myTransactionDate = new Date(); this(new Date());
} }
/** /**
@ -126,5 +134,57 @@ public class TransactionDetails {
} }
return retVal; return retVal;
} }
/**
* This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
*
* @since 5.2.0
*/
public void beginAcceptingDeferredInterceptorBroadcasts(Pointcut... thePointcuts) {
Validate.isTrue(!isAcceptingDeferredInterceptorBroadcasts());
myDeferredInterceptorBroadcasts = ArrayListMultimap.create();
myDeferredInterceptorBroadcastPointcuts = EnumSet.of(thePointcuts[0], thePointcuts);
}
/**
* This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
*
* @since 5.2.0
*/
public boolean isAcceptingDeferredInterceptorBroadcasts() {
return myDeferredInterceptorBroadcasts != null;
}
/**
* This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
*
* @since 5.2.0
*/
public boolean isAcceptingDeferredInterceptorBroadcasts(Pointcut thePointcut) {
return myDeferredInterceptorBroadcasts != null && myDeferredInterceptorBroadcastPointcuts.contains(thePointcut);
}
/**
* This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
*
* @since 5.2.0
*/
public ListMultimap<Pointcut, HookParams> endAcceptingDeferredInterceptorBroadcasts() {
Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts());
ListMultimap<Pointcut, HookParams> retVal = myDeferredInterceptorBroadcasts;
myDeferredInterceptorBroadcasts = null;
myDeferredInterceptorBroadcastPointcuts = null;
return retVal;
}
/**
* This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
*
* @since 5.2.0
*/
public void addDeferredInterceptorBroadcast(Pointcut thePointcut, HookParams theHookParams) {
Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts(thePointcut));
myDeferredInterceptorBroadcasts.put(thePointcut, theHookParams);
}
} }