Invalid ids in subscription queue (#1175)

* Start work on this

* Work on interceptors

* Attempt fix

* Avoid environment dependency

* Test fixes

* One more test fix

* One more build tweak

* Lots of cleanup

* A bit more cleanup

* Still more cleanup

* Some test fixes

* Add legacy methods temporarily

* Don't auto-scan interceptor beans

* One more test fix

* rsolve merge conflicts

* Address review comments
This commit is contained in:
James Agnew 2019-01-23 21:17:47 -05:00 committed by GitHub
parent e5723f209c
commit 38d03ea99a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1022 additions and 416 deletions

View File

@ -7,6 +7,9 @@ import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor; import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor;
import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
@ -69,6 +72,8 @@ import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.*; import javax.persistence.*;
@ -115,7 +120,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
public static final String OO_SEVERITY_INFO = "information"; public static final String OO_SEVERITY_INFO = "information";
public static final String OO_SEVERITY_WARN = "warning"; public static final String OO_SEVERITY_WARN = "warning";
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class);
private static final Map<FhirVersionEnum, FhirContext> ourRetrievalContexts = new HashMap<FhirVersionEnum, FhirContext>(); private static final Map<FhirVersionEnum, FhirContext> ourRetrievalContexts = new HashMap<>();
private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest"; private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest";
private static boolean ourValidationDisabledForUnitTest; private static boolean ourValidationDisabledForUnitTest;
private static boolean ourDisableIncrementOnUpdateForUnitTest = false; private static boolean ourDisableIncrementOnUpdateForUnitTest = false;
@ -125,6 +130,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
@Autowired @Autowired
protected IdHelperService myIdHelperService; protected IdHelperService myIdHelperService;
@Autowired @Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
protected IForcedIdDao myForcedIdDao; protected IForcedIdDao myForcedIdDao;
@Autowired @Autowired
protected ISearchResultDao mySearchResultDao; protected ISearchResultDao mySearchResultDao;
@ -1420,9 +1427,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return updateEntity(theRequest, theResource, entity, theDeletedTimestampOrNull, true, true, theUpdateTime, false, true); return updateEntity(theRequest, theResource, entity, theDeletedTimestampOrNull, true, true, theUpdateTime, false, true);
} }
public ResourceTable updateInternal(RequestDetails theRequest, T theResource, boolean thePerformIndexing, public ResourceTable updateInternal(RequestDetails theRequestDetails, T theResource, boolean thePerformIndexing, boolean theForceUpdateVersion,
boolean theForceUpdateVersion, RequestDetails theRequestDetails, ResourceTable theEntity, IIdType ResourceTable theEntity, IIdType theResourceId, IBaseResource theOldResource) {
theResourceId, IBaseResource theOldResource) {
// Notify interceptors // Notify interceptors
ActionRequestDetails requestDetails; ActionRequestDetails requestDetails;
if (theRequestDetails != null) { if (theRequestDetails != null) {
@ -1439,9 +1445,13 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
((IServerOperationInterceptor) next).resourcePreUpdate(theRequestDetails, theOldResource, theResource); ((IServerOperationInterceptor) next).resourcePreUpdate(theRequestDetails, theOldResource, theResource);
} }
} }
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theOldResource)
.add(IBaseResource.class, theResource);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED, hookParams);
// Perform update // Perform update
ResourceTable savedEntity = updateEntity(theRequest, theResource, theEntity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing); ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, theEntity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing);
/* /*
* If we aren't indexing (meaning we're probably executing a sub-operation within a transaction), * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
@ -1470,7 +1480,17 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theOldResource, theResource); ((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theOldResource, theResource);
} }
} }
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theOldResource)
.add(IBaseResource.class, theResource);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, hookParams);
}
});
} }
return savedEntity; return savedEntity;
} }
@ -1480,6 +1500,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
id = getContext().getVersion().newIdType().setValue(id.getValue()); id = getContext().getVersion().newIdType().setValue(id.getValue());
} }
if (id.hasResourceType() == false) {
id = id.withResourceType(theEntity.getResourceType());
}
theResource.setId(id); theResource.setId(id);
if (theResource instanceof IResource) { if (theResource instanceof IResource) {
ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, id.getVersionIdPart()); ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, id.getVersionIdPart());

View File

@ -26,6 +26,8 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService; import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService;
import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
@ -59,6 +61,8 @@ import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -173,7 +177,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
@Override @Override
public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> theDeleteConflicts, RequestDetails theReques) { public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> theDeleteConflicts, RequestDetails theRequest) {
if (theId == null || !theId.hasIdPart()) { if (theId == null || !theId.hasIdPart()) {
throw new InvalidRequestException("Can not perform delete, no ID provided"); throw new InvalidRequestException("Can not perform delete, no ID provided");
} }
@ -206,12 +210,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
T resourceToDelete = toResource(myResourceType, entity, null, false); T resourceToDelete = toResource(myResourceType, entity, null, false);
// Notify IServerOperationInterceptors about pre-action call // Notify IServerOperationInterceptors about pre-action call
if (theReques != null) { if (theRequest != null) {
theReques.getRequestOperationCallback().resourcePreDelete(resourceToDelete); theRequest.getRequestOperationCallback().resourcePreDelete(resourceToDelete);
} }
for (IServerInterceptor next : getConfig().getInterceptors()) { for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) { if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourcePreDelete(theReques, resourceToDelete); ((IServerOperationInterceptor) next).resourcePreDelete(theRequest, resourceToDelete);
} }
} }
@ -220,25 +224,33 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
preDelete(resourceToDelete, entity); preDelete(resourceToDelete, entity);
// Notify interceptors // Notify interceptors
if (theReques != null) { if (theRequest != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theReques, getContext(), theId.getResourceType(), theId); ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), theId.getResourceType(), theId);
notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails);
} }
Date updateTime = new Date(); Date updateTime = new Date();
ResourceTable savedEntity = updateEntity(theReques, null, entity, updateTime, updateTime); ResourceTable savedEntity = updateEntity(theRequest, null, entity, updateTime, updateTime);
resourceToDelete.setId(entity.getIdDt()); resourceToDelete.setId(entity.getIdDt());
// Notify JPA interceptors // Notify JPA interceptors
if (theReques != null) { if (theRequest != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theReques, getContext(), theId.getResourceType(), theId); ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), theId.getResourceType(), theId);
theReques.getRequestOperationCallback().resourceDeleted(resourceToDelete); theRequest.getRequestOperationCallback().resourceDeleted(resourceToDelete);
} }
for (IServerInterceptor next : getConfig().getInterceptors()) { for (IServerInterceptor next : getConfig().getInterceptors()) {
if (next instanceof IServerOperationInterceptor) { if (next instanceof IServerOperationInterceptor) {
((IServerOperationInterceptor) next).resourceDeleted(theReques, resourceToDelete); ((IServerOperationInterceptor) next).resourceDeleted(theRequest, resourceToDelete);
} }
} }
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, resourceToDelete);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED, hookParams);
}
});
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, resourceToDelete).setCreated(true); DaoMethodOutcome outcome = toMethodOutcome(savedEntity, resourceToDelete).setCreated(true);
@ -321,6 +333,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
((IServerOperationInterceptor) next).resourceDeleted(theRequest, resourceToDelete); ((IServerOperationInterceptor) next).resourceDeleted(theRequest, resourceToDelete);
} }
} }
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, resourceToDelete);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED, hookParams);
}
});
} }
IBaseOperationOutcome oo; IBaseOperationOutcome oo;
@ -423,6 +443,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
((IServerOperationInterceptor) next).resourcePreCreate(theRequest, theResource); ((IServerOperationInterceptor) next).resourcePreCreate(theRequest, theResource);
} }
} }
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theResource);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRESTORAGE_RESOURCE_CREATED, hookParams);
// Perform actual DB update // Perform actual DB update
ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing); ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing);
@ -466,6 +489,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
} }
} }
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, theResource);
myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, hookParams);
}
});
DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true); DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true);
if (!thePerformIndexing) { if (!thePerformIndexing) {
@ -753,7 +784,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return retVal; return retVal;
} }
@SuppressWarnings("JpaQlInspection")
@Override @Override
public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) { public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) {
// Notify interceptors // Notify interceptors
@ -944,8 +974,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
if (entity == null) { if (entity == null) {
if (theId.hasVersionIdPart()) { if (theId.hasVersionIdPart()) {
TypedQuery<ResourceHistoryTable> q = myEntityManager TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
q.setParameter("RID", pid); q.setParameter("RID", pid);
q.setParameter("RTYP", myResourceName); q.setParameter("RTYP", myResourceName);
q.setParameter("RVER", theId.getVersionIdPartAsLong()); q.setParameter("RVER", theId.getVersionIdPartAsLong());
@ -1305,7 +1334,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
/* /*
* Otherwise, we're not in a transaction * Otherwise, we're not in a transaction
*/ */
ResourceTable savedEntity = updateInternal(theRequestDetails, theResource, thePerformIndexing, theForceUpdateVersion, theRequestDetails, entity, resourceId, oldResource); ResourceTable savedEntity = updateInternal(theRequestDetails, theResource, thePerformIndexing, theForceUpdateVersion, entity, resourceId, oldResource);
DaoMethodOutcome outcome = toMethodOutcome(savedEntity, theResource).setCreated(false); DaoMethodOutcome outcome = toMethodOutcome(savedEntity, theResource).setCreated(false);
if (!thePerformIndexing) { if (!thePerformIndexing) {
@ -1320,13 +1349,13 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
} }
@Override @Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) { public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails); return update(theResource, theMatchUrl, true, theRequestDetails);
} }
@Override @Override
public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) { public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) {
return update(theResource, theMatchUrl, true, theRequestDetails); return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails);
} }
/** /**

View File

@ -492,7 +492,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
InstantDt deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get(nextResource); InstantDt deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get(nextResource);
Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null;
if (theUpdatedEntities.contains(nextOutcome.getEntity())) { if (theUpdatedEntities.contains(nextOutcome.getEntity())) {
updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); updateInternal(theRequestDetails, nextResource, true, false, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource());
} else if (!theNonUpdatedEntities.contains(nextOutcome.getEntity())) { } else if (!theNonUpdatedEntities.contains(nextOutcome.getEntity())) {
updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true); updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true);
} }

View File

@ -100,7 +100,6 @@ import static org.apache.commons.lang3.StringUtils.*;
* The SearchBuilder is responsible for actually forming the SQL query that handles * The SearchBuilder is responsible for actually forming the SQL query that handles
* searches for resources * searches for resources
*/ */
@SuppressWarnings("JpaQlInspection")
@Component @Component
@Scope("prototype") @Scope("prototype")
public class SearchBuilder implements ISearchBuilder { public class SearchBuilder implements ISearchBuilder {

View File

@ -89,6 +89,52 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
@Autowired @Autowired
private DaoRegistry myDaoRegistry; private DaoRegistry myDaoRegistry;
public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) {
if (theRequestDetails != null) {
IServerInterceptor.ActionRequestDetails requestDetails = new IServerInterceptor.ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
myDao.notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
}
String actionName = "Transaction";
BUNDLE response = processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, theRequest, actionName);
return response;
}
public BUNDLE collection(final RequestDetails theRequestDetails, BUNDLE theRequest) {
String transactionType = myVersionAdapter.getBundleType(theRequest);
if (!org.hl7.fhir.r4.model.Bundle.BundleType.COLLECTION.toCode().equals(transactionType)) {
throw new InvalidRequestException("Can not process collection Bundle of type: " + transactionType);
}
ourLog.info("Beginning storing collection with {} resources", myVersionAdapter.getEntries(theRequest).size());
long start = System.currentTimeMillis();
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
BUNDLE resp = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode());
List<IBaseResource> resources = new ArrayList<>();
for (final BUNDLEENTRY nextRequestEntry : myVersionAdapter.getEntries(theRequest)) {
IBaseResource resource = myVersionAdapter.getResource(nextRequestEntry);
resources.add(resource);
}
BUNDLE transactionBundle = myVersionAdapter.createBundle("transaction");
for (IBaseResource next : resources) {
BUNDLEENTRY entry = myVersionAdapter.addEntry(transactionBundle);
myVersionAdapter.setResource(entry, next);
myVersionAdapter.setRequestVerb(entry, "PUT");
myVersionAdapter.setRequestUrl(entry, next.getIdElement().toUnqualifiedVersionless().getValue());
}
transaction(theRequestDetails, transactionBundle);
return resp;
}
private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) { private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) {
myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry); myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry);
} }
@ -160,16 +206,6 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
myDao = theDao; myDao = theDao;
} }
public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) {
if (theRequestDetails != null) {
IServerInterceptor.ActionRequestDetails requestDetails = new IServerInterceptor.ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
myDao.notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
}
String actionName = "Transaction";
return processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, theRequest, actionName);
}
private BUNDLE processTransactionAsSubRequest(ServletRequestDetails theRequestDetails, BUNDLE theRequest, String theActionName) { private BUNDLE processTransactionAsSubRequest(ServletRequestDetails theRequestDetails, BUNDLE theRequest, String theActionName) {
BaseHapiFhirDao.markRequestAsProcessingSubRequest(theRequestDetails); BaseHapiFhirDao.markRequestAsProcessingSubRequest(theRequestDetails);
try { try {
@ -179,40 +215,6 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
} }
} }
public BUNDLE collection(final RequestDetails theRequestDetails, BUNDLE theRequest) {
String transactionType = myVersionAdapter.getBundleType(theRequest);
if (!org.hl7.fhir.r4.model.Bundle.BundleType.COLLECTION.toCode().equals(transactionType)) {
throw new InvalidRequestException("Can not process collection Bundle of type: " + transactionType);
}
ourLog.info("Beginning storing collection with {} resources", myVersionAdapter.getEntries(theRequest).size());
long start = System.currentTimeMillis();
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
BUNDLE resp = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode());
List<IBaseResource> resources = new ArrayList<>();
for (final BUNDLEENTRY nextRequestEntry : myVersionAdapter.getEntries(theRequest)) {
IBaseResource resource = myVersionAdapter.getResource(nextRequestEntry);
resources.add(resource);
}
BUNDLE transactionBundle = myVersionAdapter.createBundle("transaction");
for (IBaseResource next : resources) {
BUNDLEENTRY entry = myVersionAdapter.addEntry(transactionBundle);
myVersionAdapter.setResource(entry, next);
myVersionAdapter.setRequestVerb(entry, "PUT");
myVersionAdapter.setRequestUrl(entry, next.getIdElement().toUnqualifiedVersionless().getValue());
}
transaction(theRequestDetails, transactionBundle);
return resp;
}
private BUNDLE batch(final RequestDetails theRequestDetails, BUNDLE theRequest) { private BUNDLE batch(final RequestDetails theRequestDetails, BUNDLE theRequest) {
ourLog.info("Beginning batch with {} resources", myVersionAdapter.getEntries(theRequest).size()); ourLog.info("Beginning batch with {} resources", myVersionAdapter.getEntries(theRequest).size());
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
@ -234,8 +236,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
BUNDLE subRequestBundle = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode()); BUNDLE subRequestBundle = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode());
myVersionAdapter.addEntry(subRequestBundle, nextRequestEntry); myVersionAdapter.addEntry(subRequestBundle, nextRequestEntry);
BUNDLE subResponseBundle = processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request"); return processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request");
return subResponseBundle;
}; };
try { try {
@ -472,10 +473,6 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource)); return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
} }
public void setEntityManager(EntityManager theEntityManager) {
myEntityManager = theEntityManager;
}
private void validateDependencies() { private void validateDependencies() {
Validate.notNull(myEntityManager); Validate.notNull(myEntityManager);
Validate.notNull(myContext); Validate.notNull(myContext);
@ -526,7 +523,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
} }
} }
if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) { if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+:.*") && !isPlaceholder(nextResourceId)) {
throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'"); throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
} }
@ -631,7 +628,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
version = ParameterUtil.parseETagValue(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry)); version = ParameterUtil.parseETagValue(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry));
} }
res.setId(newIdType(parts.getResourceType(), parts.getResourceId(), version)); res.setId(newIdType(parts.getResourceType(), parts.getResourceId(), version));
outcome = resourceDao.update(res, null, false, theRequestDetails); outcome = resourceDao.update(res, null, false, false, theRequestDetails);
} else { } else {
res.setId((String) null); res.setId((String) null);
String matchUrl; String matchUrl;
@ -641,7 +638,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
matchUrl = parts.getResourceType(); matchUrl = parts.getResourceType();
} }
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
outcome = resourceDao.update(res, matchUrl, false, theRequestDetails); outcome = resourceDao.update(res, matchUrl, false, false, theRequestDetails);
if (Boolean.TRUE.equals(outcome.getCreated())) { if (Boolean.TRUE.equals(outcome.getCreated())) {
conditionalRequestUrls.put(matchUrl, res.getClass()); conditionalRequestUrls.put(matchUrl, res.getClass());
} }
@ -727,7 +724,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null;
if (updatedEntities.contains(nextOutcome.getEntity())) { if (updatedEntities.contains(nextOutcome.getEntity())) {
myDao.updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); myDao.updateInternal(theRequestDetails, nextResource, true, false, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource());
} else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) { } else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) {
myDao.updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true); myDao.updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true);
} }

View File

@ -99,7 +99,7 @@ public class SearchParamWithInlineReferencesExtractor {
extractInlineReferences(theResource); extractInlineReferences(theResource);
myResourceLinkExtractor.extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, myDaoResourceLinkResolver); myResourceLinkExtractor.extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, myDaoResourceLinkResolver, true);
/* /*
* If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them * If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them

View File

@ -391,7 +391,6 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
}); });
} }
@SuppressWarnings("JpaQlInspection")
private void markResourceAsIndexingFailed(final long theId) { private void markResourceAsIndexingFailed(final long theId) {
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

View File

@ -26,6 +26,9 @@ import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.interceptor.api.Hook;
import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl; import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
@ -68,7 +71,8 @@ import java.util.concurrent.TimeUnit;
*/ */
@Service @Service
@Lazy @Lazy
public class SubscriptionActivatingInterceptor extends ServerOperationInterceptorAdapter { @Interceptor(manualRegistration = true)
public class SubscriptionActivatingInterceptor {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionActivatingInterceptor.class); private Logger ourLog = LoggerFactory.getLogger(SubscriptionActivatingInterceptor.class);
private static boolean ourWaitForSubscriptionActivationSynchronouslyForUnitTest; private static boolean ourWaitForSubscriptionActivationSynchronouslyForUnitTest;
@ -160,6 +164,7 @@ public class SubscriptionActivatingInterceptor extends ServerOperationIntercepto
private boolean activateSubscription(String theActiveStatus, final IBaseResource theSubscription, String theRequestedStatus) { private boolean activateSubscription(String theActiveStatus, final IBaseResource theSubscription, String theRequestedStatus) {
IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao(); IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao();
IBaseResource subscription = subscriptionDao.read(theSubscription.getIdElement()); IBaseResource subscription = subscriptionDao.read(theSubscription.getIdElement());
subscription.setId(subscription.getIdElement().toVersionless());
ourLog.info("Activating subscription {} from status {} to {}", subscription.getIdElement().toUnqualified().getValue(), theRequestedStatus, theActiveStatus); ourLog.info("Activating subscription {} from status {} to {}", subscription.getIdElement().toUnqualified().getValue(), theRequestedStatus, theActiveStatus);
try { try {
@ -180,18 +185,18 @@ public class SubscriptionActivatingInterceptor extends ServerOperationIntercepto
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
} }
@Override @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED)
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { public void resourceCreated(IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE); submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE);
} }
@Override @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED)
public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) { public void resourceDeleted(IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE); submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE);
} }
@Override @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED)
public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
} }

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.subscription;
*/ */
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionLoader; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionLoader;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
@ -47,6 +48,8 @@ public class SubscriptionInterceptorLoader {
private SubscriptionRegistry mySubscriptionRegistry; private SubscriptionRegistry mySubscriptionRegistry;
@Autowired @Autowired
private ApplicationContext myAppicationContext; private ApplicationContext myAppicationContext;
@Autowired
private IInterceptorRegistry myInterceptorRegistry;
public void registerInterceptors() { public void registerInterceptors() {
Set<Subscription.SubscriptionChannelType> supportedSubscriptionTypes = myDaoConfig.getSupportedSubscriptionTypes(); Set<Subscription.SubscriptionChannelType> supportedSubscriptionTypes = myDaoConfig.getSupportedSubscriptionTypes();
@ -54,12 +57,12 @@ public class SubscriptionInterceptorLoader {
if (!supportedSubscriptionTypes.isEmpty()) { if (!supportedSubscriptionTypes.isEmpty()) {
loadSubscriptions(); loadSubscriptions();
ourLog.info("Registering subscription activating interceptor"); ourLog.info("Registering subscription activating interceptor");
myDaoConfig.registerInterceptor(mySubscriptionActivatingInterceptor); myInterceptorRegistry.registerInterceptor(mySubscriptionActivatingInterceptor);
} }
if (myDaoConfig.isSubscriptionMatchingEnabled()) { if (myDaoConfig.isSubscriptionMatchingEnabled()) {
mySubscriptionMatcherInterceptor.start(); mySubscriptionMatcherInterceptor.start();
ourLog.info("Registering subscription matcher interceptor"); ourLog.info("Registering subscription matcher interceptor");
myDaoConfig.registerInterceptor(mySubscriptionMatcherInterceptor); myInterceptorRegistry.registerInterceptor(mySubscriptionMatcherInterceptor);
} }
} }
@ -72,7 +75,7 @@ public class SubscriptionInterceptorLoader {
@VisibleForTesting @VisibleForTesting
void unregisterInterceptorsForUnitTest() { void unregisterInterceptorsForUnitTest() {
myDaoConfig.unregisterInterceptor(mySubscriptionActivatingInterceptor); myInterceptorRegistry.unregisterInterceptor(mySubscriptionActivatingInterceptor);
myDaoConfig.unregisterInterceptor(mySubscriptionMatcherInterceptor); myInterceptorRegistry.unregisterInterceptor(mySubscriptionMatcherInterceptor);
} }
} }

View File

@ -1,13 +1,14 @@
package ca.uhn.fhir.jpa.subscription; package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.interceptor.api.Hook;
import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.subscription.module.LinkedBlockingQueueSubscribableChannel; import ca.uhn.fhir.jpa.subscription.module.LinkedBlockingQueueSubscribableChannel;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionChannelFactory; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionChannelFactory;
import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceModifiedJsonMessage; import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceModifiedJsonMessage;
import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionMatchingSubscriber; import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionMatchingSubscriber;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
@ -42,7 +43,8 @@ import javax.annotation.PreDestroy;
@Component @Component
@Lazy @Lazy
public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAdapter implements IResourceModifiedConsumer { @Interceptor(manualRegistration = true)
public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherInterceptor.class); private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherInterceptor.class);
private static final String SUBSCRIPTION_MATCHING_CHANNEL_NAME = "subscription-matching"; private static final String SUBSCRIPTION_MATCHING_CHANNEL_NAME = "subscription-matching";
@ -82,18 +84,18 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd
} }
} }
@Override @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED)
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { public void resourceCreated(IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE); submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE);
} }
@Override @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED)
public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) { public void resourceDeleted(IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE); submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE);
} }
@Override @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED)
public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
} }
@ -115,6 +117,7 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd
/** /**
* This is an internal API - Use with caution! * This is an internal API - Use with caution!
*/ */
@Override
public void submitResourceModified(final ResourceModifiedMessage theMsg) { public void submitResourceModified(final ResourceModifiedMessage theMsg) {
sendToProcessingChannel(theMsg); sendToProcessingChannel(theMsg);
} }

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest;
import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry;
import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
@ -150,6 +151,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
protected IFhirResourceDao<RiskAssessment> myRiskAssessmentDao; protected IFhirResourceDao<RiskAssessment> myRiskAssessmentDao;
protected IServerInterceptor myInterceptor; protected IServerInterceptor myInterceptor;
@Autowired @Autowired
protected IInterceptorRegistry myInterceptorRegistry;
@Autowired
@Qualifier("myLocationDaoR4") @Qualifier("myLocationDaoR4")
protected IFhirResourceDao<Location> myLocationDao; protected IFhirResourceDao<Location> myLocationDao;
@Autowired @Autowired
@ -285,6 +288,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis()); myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
myDaoConfig.setSuppressUpdatesWithNoChange(new DaoConfig().isSuppressUpdatesWithNoChange()); myDaoConfig.setSuppressUpdatesWithNoChange(new DaoConfig().isSuppressUpdatesWithNoChange());
myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches());
myInterceptorRegistry.clearAnonymousHookForUnitTest();
} }
@After @After

View File

@ -1,159 +0,0 @@
package ca.uhn.fhir.jpa.interceptor.test;
import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry;
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.interceptor.api.Hook;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {InterceptorRegistryTest.InterceptorRegistryTestCtxConfig.class})
public class InterceptorRegistryTest {
private static boolean ourNext_beforeRestHookDelivery_Return1;
private static List<String> ourInvocations = new ArrayList<>();
private static CanonicalSubscription ourLastCanonicalSubscription;
private static ResourceDeliveryMessage ourLastResourceDeliveryMessage0;
private static ResourceDeliveryMessage ourLastResourceDeliveryMessage1;
@Autowired
private InterceptorRegistry myInterceptorRegistry;
@Test
public void testGlobalInterceptorsAreFound() {
List<Object> globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest();
assertEquals(2, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorTwo);
}
@Test
public void testInvokeGlobalInterceptorMethods() {
ResourceDeliveryMessage msg = new ResourceDeliveryMessage();
CanonicalSubscription subs = new CanonicalSubscription();
HookParams params = new HookParams(msg, subs);
boolean outcome = myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, params);
assertTrue(outcome);
assertThat(ourInvocations, contains("MyTestInterceptorOne.beforeRestHookDelivery", "MyTestInterceptorTwo.beforeRestHookDelivery"));
assertSame(msg, ourLastResourceDeliveryMessage0);
assertNull(ourLastResourceDeliveryMessage1);
assertSame(subs, ourLastCanonicalSubscription);
}
@Test
public void testInvokeGlobalInterceptorMethods_MethodAbortsProcessing() {
ourNext_beforeRestHookDelivery_Return1 = false;
ResourceDeliveryMessage msg = new ResourceDeliveryMessage();
CanonicalSubscription subs = new CanonicalSubscription();
HookParams params = new HookParams(msg, subs);
boolean outcome = myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, params);
assertFalse(outcome);
assertThat(ourInvocations, contains("MyTestInterceptorOne.beforeRestHookDelivery"));
}
@Test
public void testCallHooksInvokedWithWrongParameters() {
Integer msg = 123;
CanonicalSubscription subs = new CanonicalSubscription();
HookParams params = new HookParams(msg, subs);
try {
myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, params);
fail();
} catch (AssertionError e) {
assertEquals("Wrong hook parameters, wanted [CanonicalSubscription, ResourceDeliveryMessage] and found [CanonicalSubscription, Integer]", e.getMessage());
}
}
@Before
public void before() {
ourNext_beforeRestHookDelivery_Return1 = true;
ourLastCanonicalSubscription = null;
ourLastResourceDeliveryMessage0 = null;
ourLastResourceDeliveryMessage1 = null;
ourInvocations.clear();
}
@Configuration
@ComponentScan(basePackages = "ca.uhn.fhir.jpa.model")
static class InterceptorRegistryTestCtxConfig {
/**
* Note: Orders are deliberately reversed to make sure we get the orders right
* using the @Order annotation
*/
@Bean
public MyTestInterceptorTwo interceptor1() {
return new MyTestInterceptorTwo();
}
/**
* Note: Orders are deliberately reversed to make sure we get the orders right
* using the @Order annotation
*/
@Bean
public MyTestInterceptorOne interceptor2() {
return new MyTestInterceptorOne();
}
}
@Interceptor
@Order(100)
public static class MyTestInterceptorOne {
public MyTestInterceptorOne() {
super();
}
@Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY)
public boolean beforeRestHookDelivery(CanonicalSubscription theCanonicalSubscription) {
ourLastCanonicalSubscription = theCanonicalSubscription;
ourInvocations.add("MyTestInterceptorOne.beforeRestHookDelivery");
return ourNext_beforeRestHookDelivery_Return1;
}
}
@Interceptor
@Order(200)
public static class MyTestInterceptorTwo {
@Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY)
public void beforeRestHookDelivery(ResourceDeliveryMessage theResourceDeliveryMessage0, ResourceDeliveryMessage theResourceDeliveryMessage1) {
ourLastResourceDeliveryMessage0 = theResourceDeliveryMessage0;
ourLastResourceDeliveryMessage1 = theResourceDeliveryMessage1;
ourInvocations.add("MyTestInterceptorTwo.beforeRestHookDelivery");
}
}
/**
* Just a make-believe version of this class for the unit test
*/
private static class CanonicalSubscription {
}
/**
* Just a make-believe version of this class for the unit test
*/
private static class ResourceDeliveryMessage {
}
}

View File

@ -0,0 +1,110 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Patient;
import org.junit.AfterClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class HookInterceptorR4Test extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HookInterceptorR4Test.class);
@Test
public void testOP_PRESTORAGE_RESOURCE_CREATED_ModifyResource() {
myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.OP_PRESTORAGE_RESOURCE_CREATED, t->{
Patient contents = (Patient) t.get(IBaseResource.class, 0);
contents.getNameFirstRep().setFamily("NEWFAMILY");
});
Patient p = new Patient();
p.getNameFirstRep().setFamily("OLDFAMILY");
MethodOutcome outcome = ourClient.create().resource(p).execute();
// Response reflects change, stored resource also does
Patient responsePatient = (Patient) outcome.getResource();
assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily());
responsePatient = ourClient.read().resource(Patient.class).withId(outcome.getId()).execute();
assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily());
}
@Test
public void testOP_PRECOMMIT_RESOURCE_CREATED_ModifyResource() {
myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, t->{
Patient contents = (Patient) t.get(IBaseResource.class, 0);
contents.getNameFirstRep().setFamily("NEWFAMILY");
});
Patient p = new Patient();
p.getNameFirstRep().setFamily("OLDFAMILY");
MethodOutcome outcome = ourClient.create().resource(p).execute();
// Response reflects change, stored resource does not
Patient responsePatient = (Patient) outcome.getResource();
assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily());
responsePatient = ourClient.read().resource(Patient.class).withId(outcome.getId()).execute();
assertEquals("OLDFAMILY", responsePatient.getNameFirstRep().getFamily());
}
@Test
public void testOP_PRESTORAGE_RESOURCE_UPDATED_ModifyResource() {
Patient p = new Patient();
p.setActive(true);
IIdType id = ourClient.create().resource(p).execute().getId();
myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED, t->{
Patient contents = (Patient) t.get(IBaseResource.class, 1);
contents.getNameFirstRep().setFamily("NEWFAMILY");
});
p = new Patient();
p.setId(id);
p.getNameFirstRep().setFamily("OLDFAMILY");
MethodOutcome outcome = ourClient.update().resource(p).execute();
// Response reflects change, stored resource also does
Patient responsePatient = (Patient) outcome.getResource();
assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily());
responsePatient = ourClient.read().resource(Patient.class).withId(outcome.getId()).execute();
assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily());
}
@Test
public void testOP_PRECOMMIT_RESOURCE_UPDATED_ModifyResource() {
Patient p = new Patient();
p.setActive(true);
IIdType id = ourClient.create().resource(p).execute().getId();
myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, t->{
Patient contents = (Patient) t.get(IBaseResource.class, 1);
contents.getNameFirstRep().setFamily("NEWFAMILY");
});
p = new Patient();
p.setId(id);
p.getNameFirstRep().setFamily("OLDFAMILY");
MethodOutcome outcome = ourClient.update().resource(p).execute();
// Response reflects change, stored resource does not
Patient responsePatient = (Patient) outcome.getResource();
assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily());
responsePatient = ourClient.read().resource(Patient.class).withId(outcome.getId()).execute();
assertEquals("OLDFAMILY", responsePatient.getNameFirstRep().getFamily());
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -366,6 +366,7 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
} }
private void transaction(Bundle theBundle) throws IOException { private void transaction(Bundle theBundle) throws IOException {
String resource = myFhirCtx.newXmlParser().encodeResourceToString(theBundle); String resource = myFhirCtx.newXmlParser().encodeResourceToString(theBundle);
HttpPost post = new HttpPost(ourServerBase + "/"); HttpPost post = new HttpPost(ourServerBase + "/");

View File

@ -153,9 +153,8 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
observation.setStatus(Observation.ObservationStatus.FINAL); observation.setStatus(Observation.ObservationStatus.FINAL);
MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); IIdType id = myObservationDao.create(observation).getId();
observation.setId(id);
observation.setId(methodOutcome.getId());
return observation; return observation;
} }

View File

@ -113,10 +113,8 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test {
observation.setStatus(ObservationStatusEnum.FINAL); observation.setStatus(ObservationStatusEnum.FINAL);
MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); IIdType id = myObservationDao.create(observation).getId();
observation.setId(id);
String observationId = methodOutcome.getId().getIdPart();
observation.setId(observationId);
return observation; return observation;
} }

View File

@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.Assert.*; import static org.junit.Assert.*;
/** /**
@ -112,6 +113,39 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
} }
@Test
public void testPlaceholderReferencesInTransactionAreResolvedCorrectly() throws Exception {
String payload = "application/fhir+json";
String code = "1000000050";
String criteria1 = "Observation?";
createSubscription(criteria1, payload);
waitForActivatedSubscriptionCount(1);
// Create a transaction that should match
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
Patient patient = new Patient();
patient.setId(IdType.newRandomUuid());
patient.getIdentifierFirstRep().setSystem("foo").setValue("AAA");
bundle.addEntry().setResource(patient).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Patient");
Observation observation = new Observation();
observation.getIdentifierFirstRep().setSystem("foo").setValue("1");
observation.getCode().addCoding().setCode(code).setSystem("SNOMED-CT");
observation.setStatus(Observation.ObservationStatus.FINAL);
observation.getSubject().setReference(patient.getId());
bundle.addEntry().setResource(observation).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Observation");
// Send the transaction
mySystemDao.transaction(null, bundle);
waitForSize(1, ourUpdatedObservations);
assertThat(ourUpdatedObservations.get(0).getSubject().getReference(), matchesPattern("Patient/[0-9]+"));
}
@Test @Test
public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception { public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception {
String payload = "application/fhir+json"; String payload = "application/fhir+json";

View File

@ -136,9 +136,14 @@ public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test {
@Configuration @Configuration
static class MyTestCtxConfig { static class MyTestCtxConfig {
@Autowired
private IInterceptorRegistry myInterceptorRegistry;
@Bean @Bean
public MyTestInterceptor interceptor() { public MyTestInterceptor interceptor() {
return new MyTestInterceptor(); MyTestInterceptor retVal = new MyTestInterceptor();
myInterceptorRegistry.registerInterceptor(retVal);
return retVal;
} }
} }

View File

@ -22,11 +22,11 @@ package ca.uhn.fhir.jpa.model.interceptor.api;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
public class HookParams { public class HookParams {
@ -69,10 +69,11 @@ public class HookParams {
} }
/** /**
* Multivalued parameters will be returned twice in this list * Returns an unmodifiable multimap of the params, where the
* key is the param type and the value is the actual instance
*/ */
public List<String> getTypesAsSimpleName() { public ListMultimap<Class<?>, Object> getParamsForType() {
return myParams.values().stream().map(t -> t.getClass().getSimpleName()).collect(Collectors.toList()); return Multimaps.unmodifiableListMultimap(myParams);
} }
public Collection<Object> values() { public Collection<Object> values() {

View File

@ -0,0 +1,35 @@
package ca.uhn.fhir.jpa.model.interceptor.api;
/*-
* #%L
* HAPI FHIR Model
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public interface IInterceptorBroadcaster {
/**
* Invoke the interceptor methods
*/
boolean callHooks(Pointcut thePointcut, HookParams theParams);
/**
* Invoke the interceptor methods
*/
boolean callHooks(Pointcut thePointcut, Object... theParams);
}

View File

@ -26,6 +26,34 @@ public interface IInterceptorRegistry {
int DEFAULT_ORDER = 0; int DEFAULT_ORDER = 0;
/**
* Register an interceptor. This method has no effect if the given interceptor is already registered.
*
* @param theInterceptor The interceptor to register
* @return Returns <code>true</code> if at least one valid hook method was found on this interceptor
*/
boolean registerInterceptor(Object theInterceptor);
/**
* Unregister an interceptor. This method has no effect if the given interceptor is not already registered.
*
* @param theInterceptor The interceptor to unregister
*/
void unregisterInterceptor(Object theInterceptor);
/**
* @deprecated to be removed
*/
@Deprecated
boolean registerGlobalInterceptor(Object theInterceptor);
/**
* @deprecated to be removed
*/
@Deprecated
void unregisterGlobalInterceptor(Object theInterceptor);
@VisibleForTesting @VisibleForTesting
void registerAnonymousHookForUnitTest(Pointcut thePointcut, IAnonymousLambdaHook theHook); void registerAnonymousHookForUnitTest(Pointcut thePointcut, IAnonymousLambdaHook theHook);
@ -34,23 +62,4 @@ public interface IInterceptorRegistry {
@VisibleForTesting @VisibleForTesting
void clearAnonymousHookForUnitTest(); void clearAnonymousHookForUnitTest();
/**
* Register an interceptor
*
* @param theInterceptor The interceptor to register
* @return Returns <code>true</code> if at least one valid hook method was found on this interceptor
*/
boolean registerGlobalInterceptor(Object theInterceptor);
/**
* Invoke the interceptor methods
*/
boolean callHooks(Pointcut thePointcut, HookParams theParams);
/**
* Invoke the interceptor methods
*/
boolean callHooks(Pointcut thePointcut, Object... theParams);
} }

View File

@ -31,4 +31,11 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
public @interface Interceptor { public @interface Interceptor {
/**
* @return Declares that an interceptor should be manually registered with the registry,
* and should not auto-register using Spring autowiring.
*/
boolean manualRegistration() default false;
} }

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.model.interceptor.api;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -41,7 +41,7 @@ public enum Pointcut {
* <li>ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage</li> * <li>ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage</li>
* </ul> * </ul>
*/ */
SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY("CanonicalSubscription", "ResourceDeliveryMessage"), SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY("ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription", "ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage"),
/** /**
* Invoked immediately before the delivery of a REST HOOK subscription. * Invoked immediately before the delivery of a REST HOOK subscription.
@ -56,7 +56,7 @@ public enum Pointcut {
* <li>ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage</li> * <li>ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage</li>
* </ul> * </ul>
*/ */
SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY("CanonicalSubscription", "ResourceDeliveryMessage"), SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY("ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription", "ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage"),
/** /**
* Invoked whenever a persisted resource (a resource that has just been stored in the * Invoked whenever a persisted resource (a resource that has just been stored in the
@ -67,7 +67,7 @@ public enum Pointcut {
* <li>ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage</li> * <li>ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage</li>
* </ul> * </ul>
*/ */
SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED("ResourceModifiedMessage"), SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED("ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage"),
/** /**
@ -83,7 +83,89 @@ public enum Pointcut {
* <li>ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription</li> * <li>ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription</li>
* </ul> * </ul>
*/ */
SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED("CanonicalSubscription"); SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED("ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription"),
/**
* Invoked before a resource will be updated, immediately before the resource
* is persisted to the database.
* <p>
* Hooks will have access to the contents of the resource being created
* and may choose to make modifications to it. These changes will be
* reflected in permanent storage.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>org.hl7.fhir.instance.model.api.IBaseResource</li>
* </ul>
*/
OP_PRESTORAGE_RESOURCE_CREATED("org.hl7.fhir.instance.model.api.IBaseResource"),
/**
* Invoked before a resource will be created, immediately before the transaction
* is committed (after all validation and other business rules have successfully
* completed, and any other database activity is complete.
* <p>
* Hooks will have access to the contents of the resource being created
* but should generally not make any
* changes as storage has already occurred. Changes will not be reflected
* in storage, but may be reflected in the HTTP response.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>org.hl7.fhir.instance.model.api.IBaseResource</li>
* </ul>
*/
OP_PRECOMMIT_RESOURCE_CREATED("org.hl7.fhir.instance.model.api.IBaseResource"),
/**
* Invoked before a resource will be created
* <p>
* Hooks will have access to the contents of the resource being deleted
* but should not make any changes as storage has already occurred
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>org.hl7.fhir.instance.model.api.IBaseResource</li>
* </ul>
*/
OP_PRECOMMIT_RESOURCE_DELETED("org.hl7.fhir.instance.model.api.IBaseResource"),
/**
* Invoked before a resource will be updated, immediately before the transaction
* is committed (after all validation and other business rules have successfully
* completed, and any other database activity is complete.
* <p>
* Hooks will have access to the contents of the resource being updated
* (both the previous and new contents) but should generally not make any
* changes as storage has already occurred. Changes will not be reflected
* in storage, but may be reflected in the HTTP response.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>org.hl7.fhir.instance.model.api.IBaseResource (previous contents)</li>
* <li>org.hl7.fhir.instance.model.api.IBaseResource (new contents)</li>
* </ul>
*/
OP_PRECOMMIT_RESOURCE_UPDATED("org.hl7.fhir.instance.model.api.IBaseResource", "org.hl7.fhir.instance.model.api.IBaseResource"),
/**
* Invoked before a resource will be updated, immediately before the resource
* is persisted to the database.
* <p>
* Hooks will have access to the contents of the resource being updated
* (both the previous and new contents) and may choose to make modifications
* to the new contents of the resource. These changes will be reflected in
* permanent storage.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>org.hl7.fhir.instance.model.api.IBaseResource (previous contents)</li>
* <li>org.hl7.fhir.instance.model.api.IBaseResource (new contents)</li>
* </ul>
*/
OP_PRESTORAGE_RESOURCE_UPDATED("org.hl7.fhir.instance.model.api.IBaseResource", "org.hl7.fhir.instance.model.api.IBaseResource"),
;
private final List<String> myParameterTypes; private final List<String> myParameterTypes;
@ -94,4 +176,4 @@ public enum Pointcut {
public List<String> getParameterTypes() { public List<String> getParameterTypes() {
return myParameterTypes; return myParameterTypes;
} }
} }

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.model.interceptor.executor;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -21,45 +21,43 @@ package ca.uhn.fhir.jpa.model.interceptor.executor;
*/ */
import ca.uhn.fhir.jpa.model.interceptor.api.*; import ca.uhn.fhir.jpa.model.interceptor.api.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.PostConstruct; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Component @Component
public class InterceptorRegistry implements IInterceptorRegistry, ApplicationContextAware { public class InterceptorService implements IInterceptorRegistry, IInterceptorBroadcaster {
private static final Logger ourLog = LoggerFactory.getLogger(InterceptorRegistry.class); private static final Logger ourLog = LoggerFactory.getLogger(InterceptorService.class);
private ApplicationContext myAppCtx; private final List<Object> myInterceptors = new ArrayList<>();
private final List<Object> myGlobalInterceptors = new ArrayList<>();
private final ListMultimap<Pointcut, BaseInvoker> myInvokers = ArrayListMultimap.create(); private final ListMultimap<Pointcut, BaseInvoker> myInvokers = ArrayListMultimap.create();
private final ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); private final ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = ArrayListMultimap.create();
private final Object myRegistryMutex = new Object();
/** /**
* Constructor * Constructor
*/ */
public InterceptorRegistry() { public InterceptorService() {
super(); super();
} }
@VisibleForTesting @VisibleForTesting
public List<Object> getGlobalInterceptorsForUnitTest() { List<Object> getGlobalInterceptorsForUnitTest() {
return myGlobalInterceptors; return myInterceptors;
} }
@ -83,34 +81,43 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon
myAnonymousInvokers.clear(); myAnonymousInvokers.clear();
} }
@PostConstruct @Override
public void start() { public boolean registerInterceptor(Object theInterceptor) {
synchronized (myRegistryMutex) {
// Grab the global interceptors if (isInterceptorAlreadyRegistered(theInterceptor)) {
String[] globalInterceptorNames = myAppCtx.getBeanNamesForAnnotation(Interceptor.class); return false;
for (String nextName : globalInterceptorNames) { }
Object nextInterceptor = myAppCtx.getBean(nextName);
registerGlobalInterceptor(nextInterceptor); Class<?> interceptorClass = theInterceptor.getClass();
int typeOrder = determineOrder(interceptorClass);
if (!scanInterceptorForHookMethodsAndAddThem(theInterceptor, typeOrder)) {
return false;
}
myInterceptors.add(theInterceptor);
// Make sure we're always sorted according to the order declared in
// @Order
sortByOrderAnnotation(myInterceptors);
for (Pointcut nextPointcut : myInvokers.keys()) {
List<BaseInvoker> nextInvokerList = myInvokers.get(nextPointcut);
nextInvokerList.sort(Comparator.naturalOrder());
}
return true;
} }
} }
@Override private boolean scanInterceptorForHookMethodsAndAddThem(Object theInterceptor, int theTypeOrder) {
public boolean registerGlobalInterceptor(Object theInterceptor) {
boolean retVal = false; boolean retVal = false;
int typeOrder = DEFAULT_ORDER;
Order typeOrderAnnotation = AnnotationUtils.findAnnotation(theInterceptor.getClass(), Order.class);
if (typeOrderAnnotation != null) {
typeOrder = typeOrderAnnotation.value();
}
for (Method nextMethod : theInterceptor.getClass().getDeclaredMethods()) { for (Method nextMethod : theInterceptor.getClass().getDeclaredMethods()) {
Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class); Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class);
if (hook != null) { if (hook != null) {
int methodOrder = typeOrder; int methodOrder = theTypeOrder;
Order methodOrderAnnotation = AnnotationUtils.findAnnotation(nextMethod, Order.class); Order methodOrderAnnotation = AnnotationUtils.findAnnotation(nextMethod, Order.class);
if (methodOrderAnnotation != null) { if (methodOrderAnnotation != null) {
methodOrder = methodOrderAnnotation.value(); methodOrder = methodOrderAnnotation.value();
@ -124,20 +131,45 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon
retVal = true; retVal = true;
} }
} }
myGlobalInterceptors.add(theInterceptor);
// Make sure we're always sorted according to the order declared in
// @Order
sortByOrderAnnotation(myGlobalInterceptors);
for (Pointcut nextPointcut : myInvokers.keys()) {
List<BaseInvoker> nextInvokerList = myInvokers.get(nextPointcut);
nextInvokerList.sort(Comparator.naturalOrder());
}
return retVal; return retVal;
} }
private int determineOrder(Class<?> theInterceptorClass) {
int typeOrder = DEFAULT_ORDER;
Order typeOrderAnnotation = AnnotationUtils.findAnnotation(theInterceptorClass, Order.class);
if (typeOrderAnnotation != null) {
typeOrder = typeOrderAnnotation.value();
}
return typeOrder;
}
private boolean isInterceptorAlreadyRegistered(Object theInterceptor) {
for (Object next : myInterceptors) {
if (next == theInterceptor) {
return true;
}
}
return false;
}
@Override
public void unregisterInterceptor(Object theInterceptor) {
synchronized (myRegistryMutex) {
myInterceptors.removeIf(t -> t == theInterceptor);
myInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
}
}
@Override
public boolean registerGlobalInterceptor(Object theInterceptor) {
return registerInterceptor(theInterceptor);
}
@Override
public void unregisterGlobalInterceptor(Object theInterceptor) {
unregisterInterceptor(theInterceptor);
}
private void sortByOrderAnnotation(List<Object> theObjects) { private void sortByOrderAnnotation(List<Object> theObjects) {
IdentityHashMap<Object, Integer> interceptorToOrder = new IdentityHashMap<>(); IdentityHashMap<Object, Integer> interceptorToOrder = new IdentityHashMap<>();
for (Object next : theObjects) { for (Object next : theObjects) {
@ -154,24 +186,15 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon
} }
@Override @Override
public void setApplicationContext(@Nonnull ApplicationContext theApplicationContext) throws BeansException { public boolean callHooks(Pointcut thePointcut, Object... theParams) {
myAppCtx = theApplicationContext; return callHooks(thePointcut, new HookParams(theParams));
} }
@Override @Override
public boolean callHooks(Pointcut thePointcut, HookParams theParams) { public boolean callHooks(Pointcut thePointcut, HookParams theParams) {
assert haveAppropriateParams(thePointcut, theParams); assert haveAppropriateParams(thePointcut, theParams);
List<BaseInvoker> globalInvokers = myInvokers.get(thePointcut); List<BaseInvoker> invokers = getInvokersForPointcut(thePointcut);
List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut);
List<BaseInvoker> invokers = globalInvokers;
if (anonymousInvokers.isEmpty() == false) {
invokers = ListUtils.union(
anonymousInvokers,
globalInvokers);
invokers.sort(Comparator.naturalOrder());
}
/* /*
* Call each hook in order * Call each hook in order
@ -186,33 +209,68 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon
return true; return true;
} }
@VisibleForTesting
List<Object> getInterceptorsWithInvokersForPointcut(Pointcut thePointcut) {
return getInvokersForPointcut(thePointcut)
.stream()
.map(BaseInvoker::getInterceptor)
.collect(Collectors.toList());
}
/**
* Returns an ordered list of invokers for the given pointcut. Note that
* a new and stable list is returned to.. do whatever you want with it.
*/
private List<BaseInvoker> getInvokersForPointcut(Pointcut thePointcut) {
List<BaseInvoker> invokers;
boolean haveAnonymousInvokers;
synchronized (myRegistryMutex) {
List<BaseInvoker> globalInvokers = myInvokers.get(thePointcut);
List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut);
invokers = ListUtils.union(anonymousInvokers, globalInvokers);
haveAnonymousInvokers = anonymousInvokers.isEmpty() == false;
}
if (haveAnonymousInvokers) {
invokers.sort(Comparator.naturalOrder());
}
return invokers;
}
/** /**
* Only call this when assertions are enabled, it's expensive * Only call this when assertions are enabled, it's expensive
*/ */
private boolean haveAppropriateParams(Pointcut thePointcut, HookParams theParams) { boolean haveAppropriateParams(Pointcut thePointcut, HookParams theParams) {
List<String> givenTypes = theParams.getTypesAsSimpleName(); Validate.isTrue(theParams.getParamsForType().values().size() == thePointcut.getParameterTypes().size(), "Wrong number of params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), theParams.getParamsForType().values().stream().map(t -> t.getClass().getSimpleName()).sorted().collect(Collectors.toList()));
List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes());
givenTypes.sort(Comparator.naturalOrder());
wantedTypes.sort(Comparator.naturalOrder());
if (!givenTypes.equals(wantedTypes)) {
throw new AssertionError("Wrong hook parameters, wanted " + wantedTypes + " and found " + givenTypes);
}
return true;
}
@Override List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes());
public boolean callHooks(Pointcut thePointcut, Object... theParams) {
return callHooks(thePointcut, new HookParams(theParams)); ListMultimap<Class<?>, Object> givenTypes = theParams.getParamsForType();
for (Class<?> nextTypeClass : givenTypes.keySet()) {
String nextTypeName = nextTypeClass.getName();
for (Object nextParamValue : givenTypes.get(nextTypeClass)) {
Validate.isTrue(nextTypeClass.isAssignableFrom(nextParamValue.getClass()), "Invalid params for pointcut %s - %s is not of type %s", thePointcut.name(), nextParamValue.getClass(), nextTypeClass);
Validate.isTrue(wantedTypes.remove(nextTypeName), "Invalid params for pointcut %s - Wanted %s but missing %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), nextTypeName);
}
}
return true;
} }
private abstract class BaseInvoker implements Comparable<BaseInvoker> { private abstract class BaseInvoker implements Comparable<BaseInvoker> {
private final int myOrder; private final int myOrder;
private final Object myInterceptor;
protected BaseInvoker(int theOrder) { BaseInvoker(Object theInterceptor, int theOrder) {
myInterceptor = theInterceptor;
myOrder = theOrder; myOrder = theOrder;
} }
public Object getInterceptor() {
return myInterceptor;
}
abstract boolean invoke(HookParams theParams); abstract boolean invoke(HookParams theParams);
@Override @Override
@ -225,7 +283,7 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon
private final IAnonymousLambdaHook myHook; private final IAnonymousLambdaHook myHook;
public AnonymousLambdaInvoker(IAnonymousLambdaHook theHook, int theOrder) { public AnonymousLambdaInvoker(IAnonymousLambdaHook theHook, int theOrder) {
super(theOrder); super(theHook, theOrder);
myHook = theHook; myHook = theHook;
} }
@ -238,7 +296,6 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon
private class HookInvoker extends BaseInvoker { private class HookInvoker extends BaseInvoker {
private final Object myInterceptor;
private final boolean myReturnsBoolean; private final boolean myReturnsBoolean;
private final Method myMethod; private final Method myMethod;
private final Class<?>[] myParameterTypes; private final Class<?>[] myParameterTypes;
@ -248,8 +305,7 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon
* Constructor * Constructor
*/ */
private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) { private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) {
super(theOrder); super(theInterceptor, theOrder);
myInterceptor = theInterceptor;
myParameterTypes = theHookMethod.getParameterTypes(); myParameterTypes = theHookMethod.getParameterTypes();
myMethod = theHookMethod; myMethod = theHookMethod;
@ -285,19 +341,32 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon
// Invoke the method // Invoke the method
try { try {
Object returnValue = myMethod.invoke(myInterceptor, args); Object returnValue = myMethod.invoke(getInterceptor(), args);
if (myReturnsBoolean) { if (myReturnsBoolean) {
return (boolean) returnValue; return (boolean) returnValue;
} else { } else {
return true; return true;
} }
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
if (targetException instanceof RuntimeException) {
throw ((RuntimeException) targetException);
} else {
throw new InternalErrorException(targetException);
}
} catch (Exception e) { } catch (Exception e) {
ourLog.error("Failure executing interceptor method[{}]: {}", myMethod, e.toString(), e); throw new InternalErrorException(e);
return true;
} }
} }
} }
private static String toErrorString(List<String> theParameterTypes) {
return theParameterTypes
.stream()
.sorted()
.collect(Collectors.joining(","));
}
} }

View File

@ -0,0 +1,268 @@
package ca.uhn.fhir.jpa.model.interceptor.executor;
import ca.uhn.fhir.jpa.model.interceptor.api.*;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Patient;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {InterceptorServiceTest.InterceptorRegistryTestCtxConfig.class})
public class InterceptorServiceTest {
private static boolean ourNext_beforeRestHookDelivery_Return1;
private static List<String> ourInvocations = new ArrayList<>();
private static IBaseResource ourLastResourceOne;
private static IBaseResource ourLastResourceTwoA;
private static IBaseResource ourLastResourceTwoB;
@Autowired
private InterceptorService myInterceptorRegistry;
@Autowired
private MyTestInterceptorOne myInterceptorOne;
@Autowired
private MyTestInterceptorTwo myInterceptorTwo;
@Autowired
private MyTestInterceptorManual myInterceptorManual;
@Test
public void testGlobalInterceptorsAreFound() {
List<Object> globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest();
assertEquals(2, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorTwo);
}
@Test
public void testManuallyRegisterGlobalInterceptor() {
// Register the manual interceptor (has @Order right in the middle)
myInterceptorRegistry.registerInterceptor(myInterceptorManual);
List<Object> globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest();
assertEquals(3, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorManual);
assertTrue(globalInterceptors.get(2).getClass().toString(), globalInterceptors.get(2) instanceof MyTestInterceptorTwo);
// Try to register again (should have no effect
myInterceptorRegistry.registerInterceptor(myInterceptorManual);
globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest();
assertEquals(3, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorManual);
assertTrue(globalInterceptors.get(2).getClass().toString(), globalInterceptors.get(2) instanceof MyTestInterceptorTwo);
// Make sure we have the right invokers in the right order
List<Object> invokers = myInterceptorRegistry.getInterceptorsWithInvokersForPointcut(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED);
assertSame(myInterceptorOne, invokers.get(0));
assertSame(myInterceptorManual, invokers.get(1));
assertSame(myInterceptorTwo, invokers.get(2));
// Finally, unregister it
myInterceptorRegistry.unregisterInterceptor(myInterceptorManual);
globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest();
assertEquals(2, globalInterceptors.size());
assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne);
assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorTwo);
}
@Test
public void testInvokeGlobalInterceptorMethods() {
Patient patient = new Patient();
HookParams params = new HookParams()
.add(IBaseResource.class, patient);
boolean outcome = myInterceptorRegistry.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, params);
assertTrue(outcome);
assertThat(ourInvocations, contains("MyTestInterceptorOne.beforeRestHookDelivery", "MyTestInterceptorTwo.beforeRestHookDelivery"));
assertSame(patient, ourLastResourceTwoA);
assertNull(ourLastResourceTwoB);
assertSame(patient, ourLastResourceOne);
}
@Test
public void testInvokeGlobalInterceptorMethods_MethodAbortsProcessing() {
ourNext_beforeRestHookDelivery_Return1 = false;
Patient patient = new Patient();
HookParams params = new HookParams()
.add(IBaseResource.class, patient);
boolean outcome = myInterceptorRegistry.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, params);
assertFalse(outcome);
assertThat(ourInvocations, contains("MyTestInterceptorOne.beforeRestHookDelivery"));
}
@Test
public void testCallHooksInvokedWithWrongParameters() {
Integer msg = 123;
CanonicalSubscription subs = new CanonicalSubscription();
HookParams params = new HookParams(msg, subs);
try {
myInterceptorRegistry.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, params);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Wrong number of params for pointcut OP_PRECOMMIT_RESOURCE_CREATED - Wanted org.hl7.fhir.instance.model.api.IBaseResource but found [CanonicalSubscription, Integer]", e.getMessage());
}
}
@Test
public void testValidateParamTypes() {
HookParams params = new HookParams();
params.add(IBaseResource.class, new Patient());
params.add(IBaseResource.class, new Patient());
boolean validated = myInterceptorRegistry.haveAppropriateParams(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, params);
assertTrue(validated);
}
@Test
public void testValidateParamTypesMissingParam() {
HookParams params = new HookParams();
params.add(IBaseResource.class, new Patient());
try {
myInterceptorRegistry.haveAppropriateParams(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, params);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Wrong number of params for pointcut OP_PRECOMMIT_RESOURCE_UPDATED - Wanted org.hl7.fhir.instance.model.api.IBaseResource,org.hl7.fhir.instance.model.api.IBaseResource but found [Patient]", e.getMessage());
}
}
@Test
public void testValidateParamTypesExtraParam() {
HookParams params = new HookParams();
params.add(IBaseResource.class, new Patient());
params.add(IBaseResource.class, new Patient());
params.add(IBaseResource.class, new Patient());
try {
myInterceptorRegistry.haveAppropriateParams(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, params);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Wrong number of params for pointcut OP_PRECOMMIT_RESOURCE_UPDATED - Wanted org.hl7.fhir.instance.model.api.IBaseResource,org.hl7.fhir.instance.model.api.IBaseResource but found [Patient, Patient, Patient]", e.getMessage());
}
}
@SuppressWarnings("unchecked")
@Test
public void testValidateParamTypesWrongParam() {
HookParams params = new HookParams();
Class clazz = IBaseResource.class;
params.add(clazz, "AAA");
params.add(clazz, "BBB");
try {
myInterceptorRegistry.haveAppropriateParams(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, params);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Invalid params for pointcut OP_PRECOMMIT_RESOURCE_UPDATED - class java.lang.String is not of type interface org.hl7.fhir.instance.model.api.IBaseResource", e.getMessage());
}
}
@Before
public void before() {
ourNext_beforeRestHookDelivery_Return1 = true;
ourLastResourceOne = null;
ourLastResourceTwoA = null;
ourLastResourceTwoB = null;
ourInvocations.clear();
}
@Configuration
@ComponentScan(basePackages = "ca.uhn.fhir.jpa.model")
static class InterceptorRegistryTestCtxConfig {
@Autowired
private IInterceptorRegistry myInterceptorRegistry;
/**
* Note: Orders are deliberately reversed to make sure we get the orders right
* using the @Order annotation
*/
@Bean
public MyTestInterceptorTwo interceptor1() {
MyTestInterceptorTwo retVal = new MyTestInterceptorTwo();
myInterceptorRegistry.registerInterceptor(retVal);
return retVal;
}
/**
* Note: Orders are deliberately reversed to make sure we get the orders right
* using the @Order annotation
*/
@Bean
public MyTestInterceptorOne interceptor2() {
MyTestInterceptorOne retVal = new MyTestInterceptorOne();
myInterceptorRegistry.registerInterceptor(retVal);
return retVal;
}
@Bean
public MyTestInterceptorManual interceptorManual() {
return new MyTestInterceptorManual();
}
}
@Interceptor
@Order(100)
public static class MyTestInterceptorOne {
public MyTestInterceptorOne() {
super();
}
@Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED)
public boolean beforeRestHookDelivery(IBaseResource theResource) {
ourLastResourceOne = theResource;
ourInvocations.add("MyTestInterceptorOne.beforeRestHookDelivery");
return ourNext_beforeRestHookDelivery_Return1;
}
}
@Interceptor
@Order(300)
public static class MyTestInterceptorTwo {
@Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED)
public void beforeRestHookDelivery(IBaseResource theResource0, IBaseResource theResource1) {
ourLastResourceTwoA = theResource0;
ourLastResourceTwoB = theResource1;
ourInvocations.add("MyTestInterceptorTwo.beforeRestHookDelivery");
}
}
@Interceptor(manualRegistration = true)
@Order(200)
public static class MyTestInterceptorManual {
@Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED)
public void beforeRestHookDelivery() {
ourInvocations.add("MyTestInterceptorManual.beforeRestHookDelivery");
}
}
/**
* Just a make-believe version of this class for the unit test
*/
private static class CanonicalSubscription {
}
/**
* Just a make-believe version of this class for the unit test
*/
private static class ResourceDeliveryMessage {
}
}

View File

@ -0,0 +1,15 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.searchparam.extractor;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -58,7 +58,7 @@ public class ResourceLinkExtractor {
@Autowired @Autowired
private ISearchParamExtractor mySearchParamExtractor; private ISearchParamExtractor mySearchParamExtractor;
public void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver) { public void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, boolean theFailOnInvalidReference) {
String resourceType = theEntity.getResourceType(); String resourceType = theEntity.getResourceType();
/* /*
@ -71,13 +71,13 @@ public class ResourceLinkExtractor {
Map<String, RuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveSearchParams(toResourceName(theResource.getClass())); Map<String, RuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveSearchParams(toResourceName(theResource.getClass()));
for (RuntimeSearchParam nextSpDef : searchParams.values()) { for (RuntimeSearchParam nextSpDef : searchParams.values()) {
extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, theResourceLinkResolver, resourceType, nextSpDef); extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, theResourceLinkResolver, resourceType, nextSpDef, theFailOnInvalidReference);
} }
theEntity.setHasLinks(theParams.links.size() > 0); theEntity.setHasLinks(theParams.links.size() > 0);
} }
private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, String theResourceType, RuntimeSearchParam nextSpDef) { private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, String theResourceType, RuntimeSearchParam nextSpDef, boolean theFailOnInvalidReference) {
if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
return; return;
} }
@ -94,11 +94,11 @@ public class ResourceLinkExtractor {
List<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource, nextSpDef); List<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource, nextSpDef);
for (PathAndRef nextPathAndRef : refs) { for (PathAndRef nextPathAndRef : refs) {
extractResourceLinks(theParams, theEntity, theUpdateTime, theResourceLinkResolver, theResourceType, nextSpDef, nextPathsUnsplit, multiType, nextPathAndRef); extractResourceLinks(theParams, theEntity, theUpdateTime, theResourceLinkResolver, theResourceType, nextSpDef, nextPathsUnsplit, multiType, nextPathAndRef, theFailOnInvalidReference);
} }
} }
private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, String theResourceType, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, boolean theMultiType, PathAndRef nextPathAndRef) { private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, String theResourceType, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, boolean theMultiType, PathAndRef nextPathAndRef, boolean theFailOnInvalidReference) {
Object nextObject = nextPathAndRef.getRef(); Object nextObject = nextPathAndRef.getRef();
/* /*
@ -168,14 +168,25 @@ public class ResourceLinkExtractor {
String baseUrl = nextId.getBaseUrl(); String baseUrl = nextId.getBaseUrl();
String typeString = nextId.getResourceType(); String typeString = nextId.getResourceType();
if (isBlank(typeString)) { if (isBlank(typeString)) {
throw new InvalidRequestException("Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue()); String msg = "Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue();
if (theFailOnInvalidReference) {
throw new InvalidRequestException(msg);
} else {
ourLog.debug(msg);
return;
}
} }
RuntimeResourceDefinition resourceDefinition; RuntimeResourceDefinition resourceDefinition;
try { try {
resourceDefinition = myContext.getResourceDefinition(typeString); resourceDefinition = myContext.getResourceDefinition(typeString);
} catch (DataFormatException e) { } catch (DataFormatException e) {
throw new InvalidRequestException( String msg = "Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue();
"Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue()); if (theFailOnInvalidReference) {
throw new InvalidRequestException(msg);
} else {
ourLog.debug(msg);
return;
}
} }
if (isNotBlank(baseUrl)) { if (isNotBlank(baseUrl)) {
@ -194,7 +205,13 @@ public class ResourceLinkExtractor {
Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass(); Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
String id = nextId.getIdPart(); String id = nextId.getIdPart();
if (StringUtils.isBlank(id)) { if (StringUtils.isBlank(id)) {
throw new InvalidRequestException("Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue()); String msg = "Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue();
if (theFailOnInvalidReference) {
throw new InvalidRequestException(msg);
} else {
ourLog.debug(msg);
return;
}
} }
theResourceLinkResolver.validateTypeOrThrowException(type); theResourceLinkResolver.validateTypeOrThrowException(type);

View File

@ -47,6 +47,7 @@ public class Retrier<T> {
try { try {
return mySupplier.get(); return mySupplier.get();
} catch(RuntimeException e) { } catch(RuntimeException e) {
ourLog.trace("Failure during retry: {}", e.getMessage(), e); // with stacktrace if it's ever needed
ourLog.info("Failed to {}. Attempt {} / {}: {}", myDescription, retryCount, myMaxRetries, e.getMessage()); ourLog.info("Failed to {}. Attempt {} / {}: {}", myDescription, retryCount, myMaxRetries, e.getMessage());
lastException = e; lastException = e;
try { try {

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.subscription.module;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.subscription.module.subscriber.IResourceMessage; import ca.uhn.fhir.jpa.subscription.module.subscriber.IResourceMessage;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
@ -29,6 +30,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
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 java.util.List;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@ -112,6 +116,15 @@ public class ResourceModifiedMessage implements IResourceMessage {
} }
private void setNewPayload(FhirContext theCtx, IBaseResource theNewPayload) { private void setNewPayload(FhirContext theCtx, IBaseResource theNewPayload) {
/*
* References with placeholders would be invalid by the time we get here, and
* would be caught before we even get here. This check is basically a last-ditch
* effort to make sure nothing has broken in the various safeguards that
* should prevent this from happening (hence it only being an assert as
* opposed to something executed all the time).
*/
assert payloadContainsNoPlaceholderReferences(theCtx, theNewPayload);
/* /*
* Note: Don't set myPayloadDecoded in here- This is a false optimization since * Note: Don't set myPayloadDecoded in here- This is a false optimization since
* it doesn't actually get used if anyone is doing subscriptions at any * it doesn't actually get used if anyone is doing subscriptions at any
@ -123,7 +136,6 @@ public class ResourceModifiedMessage implements IResourceMessage {
myPayloadId = theNewPayload.getIdElement().toUnqualified().getValue(); myPayloadId = theNewPayload.getIdElement().toUnqualified().getValue();
} }
public enum OperationTypeEnum { public enum OperationTypeEnum {
CREATE, CREATE,
UPDATE, UPDATE,
@ -132,4 +144,23 @@ public class ResourceModifiedMessage implements IResourceMessage {
} }
private static boolean payloadContainsNoPlaceholderReferences(FhirContext theCtx, IBaseResource theNewPayload) {
List<ResourceReferenceInfo> refs = theCtx.newTerser().getAllResourceReferences(theNewPayload);
for (ResourceReferenceInfo next : refs) {
String ref = next.getResourceReference().getReferenceElement().getValue();
if (isBlank(ref)) {
ref = next.getResourceReference().getResource().getIdElement().getValue();
}
if (isNotBlank(ref)) {
if (ref.startsWith("#")) {
continue;
}
if (ref.startsWith("urn:uuid:")) {
throw new AssertionError("Reference at " + next.getName() + " is invalid: " + next.getResourceReference());
}
}
}
return true;
}
} }

View File

@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.subscription.module.cache;
*/ */
import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
@ -59,7 +59,7 @@ public class SubscriptionRegistry {
@Autowired @Autowired
ModelConfig myModelConfig; ModelConfig myModelConfig;
@Autowired @Autowired
private IInterceptorRegistry myInterceptorRegistry; private IInterceptorBroadcaster myInterceptorBroadcaster;
public ActiveSubscription get(String theIdPart) { public ActiveSubscription get(String theIdPart) {
return myActiveSubscriptionCache.get(theIdPart); return myActiveSubscriptionCache.get(theIdPart);
@ -101,7 +101,7 @@ public class SubscriptionRegistry {
myActiveSubscriptionCache.put(subscriptionId, activeSubscription); myActiveSubscriptionCache.put(subscriptionId, activeSubscription);
// Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED
myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, canonicalized); myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, canonicalized);
return canonicalized; return canonicalized;
} }
@ -117,7 +117,7 @@ public class SubscriptionRegistry {
unregisterAllSubscriptionsNotInCollection(Collections.emptyList()); unregisterAllSubscriptionsNotInCollection(Collections.emptyList());
} }
public void unregisterAllSubscriptionsNotInCollection(Collection<String> theAllIds) { void unregisterAllSubscriptionsNotInCollection(Collection<String> theAllIds) {
myActiveSubscriptionCache.unregisterAllSubscriptionsNotInCollection(theAllIds); myActiveSubscriptionCache.unregisterAllSubscriptionsNotInCollection(theAllIds);
} }

View File

@ -59,7 +59,7 @@ public class InMemorySubscriptionMatcher implements ISubscriptionMatcher {
entity.setResourceType(resourceType); entity.setResourceType(resourceType);
ResourceIndexedSearchParams searchParams = new ResourceIndexedSearchParams(); ResourceIndexedSearchParams searchParams = new ResourceIndexedSearchParams();
mySearchParamExtractorService.extractFromResource(searchParams, entity, resource); mySearchParamExtractorService.extractFromResource(searchParams, entity, resource);
myResourceLinkExtractor.extractResourceLinks(searchParams, entity, resource, resource.getMeta().getLastUpdated(), myInlineResourceLinkResolver); myResourceLinkExtractor.extractResourceLinks(searchParams, entity, resource, resource.getMeta().getLastUpdated(), myInlineResourceLinkResolver, false);
return myCriteriaResourceMatcher.match(criteria, resource, searchParams); return myCriteriaResourceMatcher.match(criteria, resource, searchParams);
} }
} }

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber;
* #L% * #L%
*/ */
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry;
@ -52,7 +53,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
IResourceRetriever myResourceRetriever; IResourceRetriever myResourceRetriever;
private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class); private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class);
@Autowired @Autowired
private IInterceptorRegistry myInterceptorRegistry; private IInterceptorBroadcaster myInterceptorBroadcaster;
protected void deliverPayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient) { protected void deliverPayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient) {
IBaseResource payloadResource = getAndMassagePayload(theMsg, theSubscription); IBaseResource payloadResource = getAndMassagePayload(theMsg, theSubscription);
@ -133,7 +134,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
CanonicalSubscription subscription = theMessage.getSubscription(); CanonicalSubscription subscription = theMessage.getSubscription();
// Interceptor call: SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY // Interceptor call: SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY
if (!myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, theMessage, subscription)) { if (!myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, theMessage, subscription)) {
return; return;
} }
@ -169,7 +170,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
deliverPayload(theMessage, subscription, payloadType, client); deliverPayload(theMessage, subscription, payloadType, client);
// Interceptor call: SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY // Interceptor call: SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY
if (!myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY, theMessage, subscription)) { if (!myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY, theMessage, subscription)) {
//noinspection UnnecessaryReturnStatement //noinspection UnnecessaryReturnStatement
return; return;
} }

View File

@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.subscription.module.subscriber; package ca.uhn.fhir.jpa.subscription.module.subscriber;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.cache.ActiveSubscription; import ca.uhn.fhir.jpa.subscription.module.cache.ActiveSubscription;
@ -33,9 +33,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -55,7 +55,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
@Autowired @Autowired
private SubscriptionRegistry mySubscriptionRegistry; private SubscriptionRegistry mySubscriptionRegistry;
@Autowired @Autowired
private IInterceptorRegistry myInterceptorRegistry; private IInterceptorBroadcaster myInterceptorBroadcaster;
@Override @Override
public void handleMessage(Message<?> theMessage) throws MessagingException { public void handleMessage(Message<?> theMessage) throws MessagingException {
@ -75,7 +75,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
try { try {
doMatchActiveSubscriptionsAndDeliver(theMsg); doMatchActiveSubscriptionsAndDeliver(theMsg);
} finally { } finally {
myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, theMsg); myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, theMsg);
} }
} }

View File

@ -1,16 +1,12 @@
package ca.uhn.fhir.jpa.subscription.module.config; package ca.uhn.fhir.jpa.subscription.module.config;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry;
import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionProvider; import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionProvider;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.*;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
@Configuration @Configuration
@Import(TestSubscriptionConfig.class) @Import(TestSubscriptionConfig.class)
@ComponentScan(basePackages = {"ca.uhn.fhir.jpa.model.interceptor.executor"})
public class TestSubscriptionDstu3Config extends SubscriptionDstu3Config { public class TestSubscriptionDstu3Config extends SubscriptionDstu3Config {
@Bean @Bean
@Primary @Primary
@ -24,9 +20,4 @@ public class TestSubscriptionDstu3Config extends SubscriptionDstu3Config {
return new MockFhirClientSubscriptionProvider(); return new MockFhirClientSubscriptionProvider();
} }
@Bean
public IInterceptorRegistry interceptorRegistry() {
return new InterceptorRegistry();
}
} }

View File

@ -39,6 +39,33 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test
assertFalse(result.matched()); assertFalse(result.matched());
} }
/**
* Technically this is an invalid reference in most cases, but this shouldn't choke
* the matcher in the case that it gets used.
*/
@Test
public void testPlaceholderIdInReference() {
ProcedureRequest pr = new ProcedureRequest();
pr.setId("ProcedureRequest/123");
pr.setIntent(ProcedureRequest.ProcedureRequestIntent.ORIGINALORDER);
pr.setSubject(new Reference("urn:uuid:aaaaaaaaaa"));
assertMatched(pr, "ProcedureRequest?intent=original-order");
assertNotMatched(pr, "ProcedureRequest?subject=Patient/123");
pr.setSubject(new Reference("Foo/123"));
assertMatched(pr, "ProcedureRequest?intent=original-order");
assertNotMatched(pr, "ProcedureRequest?subject=Patient/123");
pr.setSubject(new Reference("Patient/"));
assertMatched(pr, "ProcedureRequest?intent=original-order");
assertNotMatched(pr, "ProcedureRequest?subject=Patient/123");
}
@Test @Test
public void testResourceById() { public void testResourceById() {

View File

@ -2,8 +2,8 @@ package ca.uhn.fhir.jpa.subscription.module.standalone;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry;
import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test; import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test;
import ca.uhn.fhir.jpa.subscription.module.PointcutLatch; import ca.uhn.fhir.jpa.subscription.module.PointcutLatch;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
@ -52,7 +52,7 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base
@Autowired @Autowired
SubscriptionChannelFactory mySubscriptionChannelFactory; SubscriptionChannelFactory mySubscriptionChannelFactory;
@Autowired @Autowired
InterceptorRegistry myInterceptorRegistry; IInterceptorRegistry myInterceptorRegistry;
@Autowired @Autowired
protected SubscriptionRegistry mySubscriptionRegistry; protected SubscriptionRegistry mySubscriptionRegistry;