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:
parent
e5723f209c
commit
38d03ea99a
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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 + "/");
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
public void start() {
|
|
||||||
|
|
||||||
// Grab the global interceptors
|
|
||||||
String[] globalInterceptorNames = myAppCtx.getBeanNamesForAnnotation(Interceptor.class);
|
|
||||||
for (String nextName : globalInterceptorNames) {
|
|
||||||
Object nextInterceptor = myAppCtx.getBean(nextName);
|
|
||||||
registerGlobalInterceptor(nextInterceptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean registerGlobalInterceptor(Object theInterceptor) {
|
public boolean registerInterceptor(Object theInterceptor) {
|
||||||
boolean retVal = false;
|
synchronized (myRegistryMutex) {
|
||||||
|
|
||||||
int typeOrder = DEFAULT_ORDER;
|
if (isInterceptorAlreadyRegistered(theInterceptor)) {
|
||||||
Order typeOrderAnnotation = AnnotationUtils.findAnnotation(theInterceptor.getClass(), Order.class);
|
return false;
|
||||||
if (typeOrderAnnotation != null) {
|
|
||||||
typeOrder = typeOrderAnnotation.value();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean scanInterceptorForHookMethodsAndAddThem(Object theInterceptor, int theTypeOrder) {
|
||||||
|
boolean retVal = false;
|
||||||
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,18 +131,43 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon
|
||||||
retVal = true;
|
retVal = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return retVal;
|
||||||
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;
|
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) {
|
||||||
|
@ -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());
|
List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes());
|
||||||
givenTypes.sort(Comparator.naturalOrder());
|
|
||||||
wantedTypes.sort(Comparator.naturalOrder());
|
ListMultimap<Class<?>, Object> givenTypes = theParams.getParamsForType();
|
||||||
if (!givenTypes.equals(wantedTypes)) {
|
for (Class<?> nextTypeClass : givenTypes.keySet()) {
|
||||||
throw new AssertionError("Wrong hook parameters, wanted " + wantedTypes + " and found " + givenTypes);
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
return true;
|
||||||
public boolean callHooks(Pointcut thePointcut, Object... theParams) {
|
|
||||||
return callHooks(thePointcut, new HookParams(theParams));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(","));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue