R5 Subscriptions (#4748)
* comments from conversation with Gino * rewrite R5 subscription canonicalization. Expect the r5 subscription tests to fail now * SubscriptionTopicR5Test passes now * R4B tests pass now * first two tests in RestHookTestR5Test now pass. just need to convert the rest * third test passes * fourth test passes * tests pass up to line 294 * wow what a marathon. Turns out when we stripped the version we didn't remove it from the meta version. * passes up to 427 * RestHookTestR5Test tests pass up to line 582 * RestHookTestR5Test tests pass up to line 591 Added SubscriptionTopicRegisteringSubscriber * RestHookTestR5Test tests pass up to line 591 Added SubscriptionTopicRegisteringSubscriber * RestHookTestR5Test tests pass up to line 636 Added SubscriptionTopicValidatingInterceptor * RestHookTestR5Test tests pass up to line 689 * RestHookTestR5Test tests pass up to line 758 * 4 failures left * woohoo all tests pass * all tests pass and no PointCutLatch errors * Msg.code * changelog * checkstyle * fix some tests * compile issue * fix test * fix regression * fix test * R5 currently runs tests in multiple threads, so change the sensitive one to an IT * licenses * review feedback --------- Co-authored-by: Ken Stevens <ken@smilecdr.com>
This commit is contained in:
parent
8813d9beda
commit
6b9af3291e
|
@ -24,6 +24,7 @@ import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.util.ParametersUtil;
|
import ca.uhn.fhir.util.ParametersUtil;
|
||||||
import org.hl7.fhir.instance.model.api.IBase;
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
|
|
||||||
public class BundleEntryMutator {
|
public class BundleEntryMutator {
|
||||||
|
@ -57,4 +58,9 @@ public class BundleEntryMutator {
|
||||||
BaseRuntimeChildDefinition fullUrlChild = myEntryDefinition.getChildByName("fullUrl");
|
BaseRuntimeChildDefinition fullUrlChild = myEntryDefinition.getChildByName("fullUrl");
|
||||||
fullUrlChild.getMutator().setValue(myEntry, value);
|
fullUrlChild.getMutator().setValue(myEntry, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setResource(IBaseResource theUpdatedResource) {
|
||||||
|
BaseRuntimeChildDefinition resourceChild = myEntryDefinition.getChildByName("resource");
|
||||||
|
resourceChild.getMutator().setValue(myEntry, theUpdatedResource);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,4 +54,8 @@ public class ModifiableBundleEntry {
|
||||||
public void setFullUrl(String theFullUrl) {
|
public void setFullUrl(String theFullUrl) {
|
||||||
myBundleEntryMutator.setFullUrl(theFullUrl);
|
myBundleEntryMutator.setFullUrl(theFullUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setResource(IBaseResource theUpdatedResource) {
|
||||||
|
myBundleEntryMutator.setResource(theUpdatedResource);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
type: change
|
||||||
|
issue: 4748
|
||||||
|
title: "Previously, HAPI-FHIR converted R5 Subscriptions into R4 Subscriptions and triggered those subscriptions by
|
||||||
|
resource changes in the same way R4 subscriptions are triggered. Now R5 Subscriptions are triggered based on the topic
|
||||||
|
they subscribe to and the resource matching happens via the SubscriptionTopic resource. This also means that
|
||||||
|
R5 Subscription endpoints are now delivered a subscription-notification Bundle as opposed to the resource as is the
|
||||||
|
case with R4 Subscriptions."
|
|
@ -43,6 +43,7 @@ import ca.uhn.fhir.rest.gclient.IClientExecutable;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||||
import ca.uhn.fhir.rest.server.messaging.BaseResourceModifiedMessage;
|
import ca.uhn.fhir.rest.server.messaging.BaseResourceModifiedMessage;
|
||||||
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
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;
|
||||||
|
@ -87,7 +88,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
||||||
IClientExecutable<?, ?> operation;
|
IClientExecutable<?, ?> operation;
|
||||||
|
|
||||||
if (theSubscription.isTopicSubscription()) {
|
if (theSubscription.isTopicSubscription()) {
|
||||||
operation = createDeliveryRequestTopic((IBaseBundle) theMsg.getPayload(myFhirContext), theClient, thePayloadResource);
|
operation = createDeliveryRequestTopic((IBaseBundle) thePayloadResource, theClient);
|
||||||
} else if (isNotBlank(theSubscription.getPayloadSearchCriteria())) {
|
} else if (isNotBlank(theSubscription.getPayloadSearchCriteria())) {
|
||||||
operation = createDeliveryRequestTransaction(theSubscription, theClient, thePayloadResource);
|
operation = createDeliveryRequestTransaction(theSubscription, theClient, thePayloadResource);
|
||||||
} else if (thePayloadType != null) {
|
} else if (thePayloadType != null) {
|
||||||
|
@ -141,43 +142,76 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
||||||
return theClient.transaction().withBundle(bundle);
|
return theClient.transaction().withBundle(bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IClientExecutable<?, ?> createDeliveryRequestTopic(IBaseBundle theBundle, IGenericClient theClient, IBaseResource thePayloadResource) {
|
private IClientExecutable<?, ?> createDeliveryRequestTopic(IBaseBundle theBundle, IGenericClient theClient) {
|
||||||
return theClient.transaction().withBundle(theBundle);
|
return theClient.transaction().withBundle(theBundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IBaseResource getResource(IIdType payloadId, RequestPartitionId thePartitionId, boolean theDeletedOK) throws ResourceGoneException {
|
public IBaseResource getResource(IIdType thePayloadId, RequestPartitionId thePartitionId, boolean theDeletedOK) throws ResourceGoneException {
|
||||||
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(payloadId.getResourceType());
|
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(thePayloadId.getResourceType());
|
||||||
SystemRequestDetails systemRequestDetails = new SystemRequestDetails().setRequestPartitionId(thePartitionId);
|
SystemRequestDetails systemRequestDetails = new SystemRequestDetails().setRequestPartitionId(thePartitionId);
|
||||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceDef.getImplementingClass());
|
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceDef.getImplementingClass());
|
||||||
return dao.read(payloadId.toVersionless(), systemRequestDetails, theDeletedOK);
|
return dao.read(thePayloadId.toVersionless(), systemRequestDetails, theDeletedOK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform operations on the payload based on various subscription extension settings such as deliver latest version,
|
||||||
|
* delete and/or strip version id.
|
||||||
|
* @param theMsg
|
||||||
|
* @param theSubscription
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
protected IBaseResource getAndMassagePayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription) {
|
protected IBaseResource getAndMassagePayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription) {
|
||||||
IBaseResource payloadResource = theMsg.getPayload(myFhirContext);
|
IBaseResource payloadResource = theMsg.getPayload(myFhirContext);
|
||||||
|
|
||||||
if (payloadResource == null || theSubscription.getRestHookDetails().isDeliverLatestVersion()) {
|
if (payloadResource instanceof IBaseBundle) {
|
||||||
IIdType payloadId = theMsg.getPayloadId(myFhirContext);
|
return getAndMassageBundle(theMsg, (IBaseBundle) payloadResource, theSubscription);
|
||||||
|
} else {
|
||||||
|
return getAndMassageResource(theMsg, payloadResource, theSubscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IBaseResource getAndMassageBundle(ResourceDeliveryMessage theMsg, IBaseBundle theBundle, CanonicalSubscription theSubscription) {
|
||||||
|
BundleUtil.processEntries(myFhirContext, theBundle, entry -> {
|
||||||
|
IBaseResource entryResource = entry.getResource();
|
||||||
|
if (entryResource != null) {
|
||||||
|
// SubscriptionStatus is a "virtual" resource type that is not stored in the repository
|
||||||
|
if (!"SubscriptionStatus".equals(myFhirContext.getResourceType(entryResource))) {
|
||||||
|
IBaseResource updatedResource = getAndMassageResource(theMsg, entryResource, theSubscription);
|
||||||
|
entry.setFullUrl(updatedResource.getIdElement().getValue());
|
||||||
|
entry.setResource(updatedResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return theBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IBaseResource getAndMassageResource(ResourceDeliveryMessage theMsg, IBaseResource thePayloadResource, CanonicalSubscription theSubscription) {
|
||||||
|
if (thePayloadResource == null || theSubscription.getRestHookDetails().isDeliverLatestVersion()) {
|
||||||
|
|
||||||
|
IIdType payloadId = theMsg.getPayloadId(myFhirContext).toVersionless();
|
||||||
|
if (theSubscription.isTopicSubscription()) {
|
||||||
|
payloadId = thePayloadResource.getIdElement().toVersionless();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (payloadId != null) {
|
if (payloadId != null) {
|
||||||
boolean deletedOK = theMsg.getOperationType() == BaseResourceModifiedMessage.OperationTypeEnum.DELETE;
|
boolean deletedOK = theMsg.getOperationType() == BaseResourceModifiedMessage.OperationTypeEnum.DELETE;
|
||||||
payloadResource = getResource(payloadId.toVersionless(), theMsg.getRequestPartitionId(), deletedOK);
|
thePayloadResource = getResource(payloadId, theMsg.getRequestPartitionId(), deletedOK);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (ResourceGoneException e) {
|
} catch (ResourceGoneException e) {
|
||||||
ourLog.warn("Resource {} is deleted, not going to deliver for subscription {}", payloadId.toVersionless(), theSubscription.getIdElement(myFhirContext));
|
ourLog.warn("Resource {} is deleted, not going to deliver for subscription {}", payloadId, theSubscription.getIdElement(myFhirContext));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IIdType resourceId = payloadResource.getIdElement();
|
IIdType resourceId = thePayloadResource.getIdElement();
|
||||||
if (theSubscription.getRestHookDetails().isStripVersionId()) {
|
if (theSubscription.getRestHookDetails().isStripVersionId()) {
|
||||||
resourceId = resourceId.toVersionless();
|
resourceId = resourceId.toVersionless();
|
||||||
payloadResource.setId(resourceId);
|
thePayloadResource.setId(resourceId);
|
||||||
|
thePayloadResource.getMeta().setVersionId(null);
|
||||||
}
|
}
|
||||||
return payloadResource;
|
return thePayloadResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*-
|
|
||||||
* #%L
|
|
||||||
* HAPI FHIR Subscription Server
|
|
||||||
* %%
|
|
||||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
|
||||||
* %%
|
|
||||||
* 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%
|
|
||||||
*/
|
|
||||||
package ca.uhn.fhir.jpa.subscription.match.matcher.subscriber;
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
|
||||||
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
|
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.messaging.MessageHandler;
|
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
|
||||||
|
|
||||||
public abstract class BaseSubscriberForSubscriptionResources implements MessageHandler {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
protected FhirContext myFhirContext;
|
|
||||||
|
|
||||||
protected boolean isSubscription(ResourceModifiedMessage theNewResource) {
|
|
||||||
String payloadIdType = null;
|
|
||||||
IIdType payloadId = theNewResource.getPayloadId(myFhirContext);
|
|
||||||
if (payloadId != null) {
|
|
||||||
payloadIdType = payloadId.getResourceType();
|
|
||||||
}
|
|
||||||
if (isBlank(payloadIdType)) {
|
|
||||||
IBaseResource payload = theNewResource.getNewPayload(myFhirContext);
|
|
||||||
if (payload != null) {
|
|
||||||
payloadIdType = myFhirContext.getResourceType(payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResourceTypeEnum.SUBSCRIPTION.getCode().equals(payloadIdType);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.subscription.channel.api.ChannelConsumerSettings;
|
||||||
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelReceiver;
|
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelReceiver;
|
||||||
import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory;
|
import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory;
|
||||||
import ca.uhn.fhir.jpa.topic.SubscriptionTopicMatchingSubscriber;
|
import ca.uhn.fhir.jpa.topic.SubscriptionTopicMatchingSubscriber;
|
||||||
|
import ca.uhn.fhir.jpa.topic.SubscriptionTopicRegisteringSubscriber;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -50,6 +51,8 @@ public class MatchingQueueSubscriberLoader {
|
||||||
private SubscriptionChannelFactory mySubscriptionChannelFactory;
|
private SubscriptionChannelFactory mySubscriptionChannelFactory;
|
||||||
@Autowired
|
@Autowired
|
||||||
private SubscriptionRegisteringSubscriber mySubscriptionRegisteringSubscriber;
|
private SubscriptionRegisteringSubscriber mySubscriptionRegisteringSubscriber;
|
||||||
|
@Autowired(required = false)
|
||||||
|
private SubscriptionTopicRegisteringSubscriber mySubscriptionTopicRegisteringSubscriber;
|
||||||
@Autowired
|
@Autowired
|
||||||
private SubscriptionActivatingSubscriber mySubscriptionActivatingSubscriber;
|
private SubscriptionActivatingSubscriber mySubscriptionActivatingSubscriber;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -70,6 +73,9 @@ public class MatchingQueueSubscriberLoader {
|
||||||
ourLog.info("Starting SubscriptionTopic Matching Subscriber");
|
ourLog.info("Starting SubscriptionTopic Matching Subscriber");
|
||||||
myMatchingChannel.subscribe(mySubscriptionTopicMatchingSubscriber);
|
myMatchingChannel.subscribe(mySubscriptionTopicMatchingSubscriber);
|
||||||
}
|
}
|
||||||
|
if (mySubscriptionTopicRegisteringSubscriber != null) {
|
||||||
|
myMatchingChannel.subscribe(mySubscriptionTopicRegisteringSubscriber);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,16 +19,16 @@
|
||||||
*/
|
*/
|
||||||
package ca.uhn.fhir.jpa.subscription.match.matcher.subscriber;
|
package ca.uhn.fhir.jpa.subscription.match.matcher.subscriber;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||||
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
||||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
|
||||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionConstants;
|
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionConstants;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
||||||
|
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
import ca.uhn.fhir.util.SubscriptionUtil;
|
import ca.uhn.fhir.util.SubscriptionUtil;
|
||||||
|
@ -49,9 +49,11 @@ import javax.annotation.Nonnull;
|
||||||
* <p>
|
* <p>
|
||||||
* Also validates criteria. If invalid, rejects the subscription without persisting the subscription.
|
* Also validates criteria. If invalid, rejects the subscription without persisting the subscription.
|
||||||
*/
|
*/
|
||||||
public class SubscriptionActivatingSubscriber extends BaseSubscriberForSubscriptionResources implements MessageHandler {
|
public class SubscriptionActivatingSubscriber implements MessageHandler {
|
||||||
private final Logger ourLog = LoggerFactory.getLogger(SubscriptionActivatingSubscriber.class);
|
private final Logger ourLog = LoggerFactory.getLogger(SubscriptionActivatingSubscriber.class);
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private FhirContext myFhirContext;
|
||||||
|
@Autowired
|
||||||
private DaoRegistry myDaoRegistry;
|
private DaoRegistry myDaoRegistry;
|
||||||
@Autowired
|
@Autowired
|
||||||
private SubscriptionCanonicalizer mySubscriptionCanonicalizer;
|
private SubscriptionCanonicalizer mySubscriptionCanonicalizer;
|
||||||
|
@ -73,7 +75,7 @@ public class SubscriptionActivatingSubscriber extends BaseSubscriberForSubscript
|
||||||
}
|
}
|
||||||
|
|
||||||
ResourceModifiedMessage payload = ((ResourceModifiedJsonMessage) theMessage).getPayload();
|
ResourceModifiedMessage payload = ((ResourceModifiedJsonMessage) theMessage).getPayload();
|
||||||
if (!isSubscription(payload)) {
|
if (!payload.hasPayloadType(myFhirContext, "Subscription")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
|
||||||
private void doMatchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) {
|
private void doMatchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) {
|
||||||
IIdType resourceId = theMsg.getPayloadId(myFhirContext);
|
IIdType resourceId = theMsg.getPayloadId(myFhirContext);
|
||||||
|
|
||||||
Collection<ActiveSubscription> subscriptions = mySubscriptionRegistry.getAll();
|
Collection<ActiveSubscription> subscriptions = mySubscriptionRegistry.getAllNonTopicSubscriptions();
|
||||||
|
|
||||||
ourLog.trace("Testing {} subscriptions for applicability", subscriptions.size());
|
ourLog.trace("Testing {} subscriptions for applicability", subscriptions.size());
|
||||||
boolean anySubscriptionsMatchedResource = false;
|
boolean anySubscriptionsMatchedResource = false;
|
||||||
|
|
|
@ -23,12 +23,12 @@ import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
|
||||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
|
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||||
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;
|
||||||
|
@ -47,7 +47,7 @@ import javax.annotation.Nonnull;
|
||||||
* <p>
|
* <p>
|
||||||
* Also validates criteria. If invalid, rejects the subscription without persisting the subscription.
|
* Also validates criteria. If invalid, rejects the subscription without persisting the subscription.
|
||||||
*/
|
*/
|
||||||
public class SubscriptionRegisteringSubscriber extends BaseSubscriberForSubscriptionResources implements MessageHandler {
|
public class SubscriptionRegisteringSubscriber implements MessageHandler {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionRegisteringSubscriber.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionRegisteringSubscriber.class);
|
||||||
@Autowired
|
@Autowired
|
||||||
private FhirContext myFhirContext;
|
private FhirContext myFhirContext;
|
||||||
|
@ -74,7 +74,7 @@ public class SubscriptionRegisteringSubscriber extends BaseSubscriberForSubscrip
|
||||||
|
|
||||||
ResourceModifiedMessage payload = ((ResourceModifiedJsonMessage) theMessage).getPayload();
|
ResourceModifiedMessage payload = ((ResourceModifiedJsonMessage) theMessage).getPayload();
|
||||||
|
|
||||||
if (!isSubscription(payload)) {
|
if (!payload.hasPayloadType(this.myFhirContext, "Subscription")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,11 +85,22 @@ class ActiveSubscriptionCache {
|
||||||
return retval;
|
return retval;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ActiveSubscription> getTopicSubscriptionsForUrl(String theUrl) {
|
/**
|
||||||
assert !isBlank(theUrl);
|
* R4B and R5 only
|
||||||
|
* @param theTopic
|
||||||
|
* @return a list of all subscriptions that are subscribed to the given topic
|
||||||
|
*/
|
||||||
|
public List<ActiveSubscription> getTopicSubscriptionsForTopic(String theTopic) {
|
||||||
|
assert !isBlank(theTopic);
|
||||||
return getAll().stream()
|
return getAll().stream()
|
||||||
.filter(as -> as.getSubscription().isTopicSubscription())
|
.filter(as -> as.getSubscription().isTopicSubscription())
|
||||||
.filter(as -> theUrl.equals(as.getSubscription().getCriteriaString()))
|
.filter(as -> theTopic.equals(as.getSubscription().getTopic()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ActiveSubscription> getAllNonTopicSubscriptions() {
|
||||||
|
return getAll().stream()
|
||||||
|
.filter(as -> !as.getSubscription().isTopicSubscription())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,8 +74,8 @@ public class SubscriptionRegistry {
|
||||||
return myActiveSubscriptionCache.getAll();
|
return myActiveSubscriptionCache.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized List<ActiveSubscription> getTopicSubscriptionsByUrl(String theUrl) {
|
public synchronized List<ActiveSubscription> getTopicSubscriptionsByTopic(String theTopic) {
|
||||||
return myActiveSubscriptionCache.getTopicSubscriptionsForUrl(theUrl);
|
return myActiveSubscriptionCache.getTopicSubscriptionsForTopic(theTopic);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<CanonicalSubscription> hasSubscription(IIdType theId) {
|
private Optional<CanonicalSubscription> hasSubscription(IIdType theId) {
|
||||||
|
@ -213,4 +213,8 @@ public class SubscriptionRegistry {
|
||||||
public int size() {
|
public int size() {
|
||||||
return myActiveSubscriptionCache.size();
|
return myActiveSubscriptionCache.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized List<ActiveSubscription> getAllNonTopicSubscriptions() {
|
||||||
|
return myActiveSubscriptionCache.getAllNonTopicSubscriptions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,11 @@
|
||||||
*/
|
*/
|
||||||
package ca.uhn.fhir.jpa.subscription.submit.config;
|
package ca.uhn.fhir.jpa.subscription.submit.config;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.config.SubscriptionModelConfig;
|
import ca.uhn.fhir.jpa.subscription.model.config.SubscriptionModelConfig;
|
||||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor;
|
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator;
|
||||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionSubmitInterceptorLoader;
|
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionSubmitInterceptorLoader;
|
||||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionValidatingInterceptor;
|
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionValidatingInterceptor;
|
||||||
import ca.uhn.fhir.jpa.subscription.triggering.ISubscriptionTriggeringSvc;
|
import ca.uhn.fhir.jpa.subscription.triggering.ISubscriptionTriggeringSvc;
|
||||||
|
@ -48,6 +51,11 @@ public class SubscriptionSubmitterConfig {
|
||||||
return new SubscriptionValidatingInterceptor();
|
return new SubscriptionValidatingInterceptor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SubscriptionQueryValidator subscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
|
||||||
|
return new SubscriptionQueryValidator(theDaoRegistry, theSubscriptionStrategyEvaluator);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SubscriptionSubmitInterceptorLoader subscriptionMatcherInterceptorLoader() {
|
public SubscriptionSubmitInterceptorLoader subscriptionMatcherInterceptorLoader() {
|
||||||
return new SubscriptionSubmitInterceptorLoader();
|
return new SubscriptionSubmitInterceptorLoader();
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR Subscription Server
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* 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%
|
||||||
|
*/
|
||||||
|
package ca.uhn.fhir.jpa.subscription.submit.interceptor;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionCriteriaParser;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
|
|
||||||
|
public class SubscriptionQueryValidator {
|
||||||
|
private final DaoRegistry myDaoRegistry;
|
||||||
|
private final SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator;
|
||||||
|
|
||||||
|
public SubscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
|
||||||
|
myDaoRegistry = theDaoRegistry;
|
||||||
|
mySubscriptionStrategyEvaluator = theSubscriptionStrategyEvaluator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateCriteria(String theCriteria, String theFieldName) {
|
||||||
|
if (isBlank(theCriteria)) {
|
||||||
|
throw new UnprocessableEntityException(Msg.code(11) + theFieldName + " must be populated");
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscriptionCriteriaParser.SubscriptionCriteria parsedCriteria = SubscriptionCriteriaParser.parse(theCriteria);
|
||||||
|
if (parsedCriteria == null) {
|
||||||
|
throw new UnprocessableEntityException(Msg.code(12) + theFieldName + " can not be parsed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedCriteria.getType() == SubscriptionCriteriaParser.TypeEnum.STARTYPE_EXPRESSION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String next : parsedCriteria.getApplicableResourceTypes()) {
|
||||||
|
if (!myDaoRegistry.isResourceTypeSupported(next)) {
|
||||||
|
throw new UnprocessableEntityException(Msg.code(13) + theFieldName + " contains invalid/unsupported resource type: " + next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedCriteria.getType() != SubscriptionCriteriaParser.TypeEnum.SEARCH_EXPRESSION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sep = theCriteria.indexOf('?');
|
||||||
|
if (sep <= 1) {
|
||||||
|
throw new UnprocessableEntityException(Msg.code(14) + theFieldName + " must be in the form \"{Resource Type}?[params]\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
String resType = theCriteria.substring(0, sep);
|
||||||
|
if (resType.contains("/")) {
|
||||||
|
throw new UnprocessableEntityException(Msg.code(15) + theFieldName + " must be in the form \"{Resource Type}?[params]\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SubscriptionMatchingStrategy determineStrategy(String theCriteriaString) {
|
||||||
|
return mySubscriptionStrategyEvaluator.determineStrategy(theCriteriaString);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.subscription.submit.interceptor;
|
||||||
|
|
||||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||||
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
||||||
|
import ca.uhn.fhir.jpa.topic.SubscriptionTopicValidatingInterceptor;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import org.hl7.fhir.dstu2.model.Subscription;
|
import org.hl7.fhir.dstu2.model.Subscription;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -37,12 +38,15 @@ public class SubscriptionSubmitInterceptorLoader {
|
||||||
private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
|
private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
|
||||||
@Autowired
|
@Autowired
|
||||||
private SubscriptionValidatingInterceptor mySubscriptionValidatingInterceptor;
|
private SubscriptionValidatingInterceptor mySubscriptionValidatingInterceptor;
|
||||||
|
@Autowired(required = false)
|
||||||
|
private SubscriptionTopicValidatingInterceptor mySubscriptionTopicValidatingInterceptor;
|
||||||
@Autowired
|
@Autowired
|
||||||
private StorageSettings myStorageSettings;
|
private StorageSettings myStorageSettings;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IInterceptorService myInterceptorRegistry;
|
private IInterceptorService myInterceptorRegistry;
|
||||||
private boolean mySubscriptionValidatingInterceptorRegistered;
|
private boolean mySubscriptionValidatingInterceptorRegistered;
|
||||||
private boolean mySubscriptionMatcherInterceptorRegistered;
|
private boolean mySubscriptionMatcherInterceptorRegistered;
|
||||||
|
private boolean mySubscriptionTopicValidatingInterceptorRegistered;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void start() {
|
public void start() {
|
||||||
|
@ -62,6 +66,11 @@ public class SubscriptionSubmitInterceptorLoader {
|
||||||
myInterceptorRegistry.registerInterceptor(mySubscriptionValidatingInterceptor);
|
myInterceptorRegistry.registerInterceptor(mySubscriptionValidatingInterceptor);
|
||||||
mySubscriptionValidatingInterceptorRegistered = true;
|
mySubscriptionValidatingInterceptorRegistered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mySubscriptionTopicValidatingInterceptor != null && !mySubscriptionTopicValidatingInterceptorRegistered) {
|
||||||
|
myInterceptorRegistry.registerInterceptor(mySubscriptionTopicValidatingInterceptor);
|
||||||
|
mySubscriptionTopicValidatingInterceptorRegistered = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
|
@ -33,7 +33,6 @@ import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator;
|
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionCriteriaParser;
|
|
||||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||||
|
@ -73,6 +72,8 @@ public class SubscriptionValidatingInterceptor {
|
||||||
private FhirContext myFhirContext;
|
private FhirContext myFhirContext;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
|
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
|
||||||
|
@Autowired
|
||||||
|
private SubscriptionQueryValidator mySubscriptionQueryValidator;
|
||||||
|
|
||||||
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
|
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
|
||||||
public void resourcePreCreate(IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
|
public void resourcePreCreate(IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
|
||||||
|
@ -146,9 +147,9 @@ public class SubscriptionValidatingInterceptor {
|
||||||
if (!finished) {
|
if (!finished) {
|
||||||
|
|
||||||
if (subscription.isTopicSubscription()) {
|
if (subscription.isTopicSubscription()) {
|
||||||
Optional<IBaseResource> oTopic = findSubscriptionTopicByUrl(subscription.getCriteriaString());
|
Optional<IBaseResource> oTopic = findSubscriptionTopicByUrl(subscription.getTopic());
|
||||||
if (!oTopic.isPresent()) {
|
if (!oTopic.isPresent()) {
|
||||||
throw new UnprocessableEntityException(Msg.code(2322) + "No SubscriptionTopic exists with url: " + subscription.getCriteriaString());
|
throw new UnprocessableEntityException(Msg.code(2322) + "No SubscriptionTopic exists with topic: " + subscription.getTopic());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
validateQuery(subscription.getCriteriaString(), "Subscription.criteria");
|
validateQuery(subscription.getCriteriaString(), "Subscription.criteria");
|
||||||
|
@ -217,39 +218,7 @@ public class SubscriptionValidatingInterceptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateQuery(String theQuery, String theFieldName) {
|
public void validateQuery(String theQuery, String theFieldName) {
|
||||||
if (isBlank(theQuery)) {
|
mySubscriptionQueryValidator.validateCriteria(theQuery, theFieldName);
|
||||||
throw new UnprocessableEntityException(Msg.code(11) + theFieldName + " must be populated");
|
|
||||||
}
|
|
||||||
|
|
||||||
SubscriptionCriteriaParser.SubscriptionCriteria parsedCriteria = SubscriptionCriteriaParser.parse(theQuery);
|
|
||||||
if (parsedCriteria == null) {
|
|
||||||
throw new UnprocessableEntityException(Msg.code(12) + theFieldName + " can not be parsed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedCriteria.getType() == SubscriptionCriteriaParser.TypeEnum.STARTYPE_EXPRESSION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String next : parsedCriteria.getApplicableResourceTypes()) {
|
|
||||||
if (!myDaoRegistry.isResourceTypeSupported(next)) {
|
|
||||||
throw new UnprocessableEntityException(Msg.code(13) + theFieldName + " contains invalid/unsupported resource type: " + next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedCriteria.getType() != SubscriptionCriteriaParser.TypeEnum.SEARCH_EXPRESSION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sep = theQuery.indexOf('?');
|
|
||||||
if (sep <= 1) {
|
|
||||||
throw new UnprocessableEntityException(Msg.code(14) + theFieldName + " must be in the form \"{Resource Type}?[params]\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
String resType = theQuery.substring(0, sep);
|
|
||||||
if (resType.contains("/")) {
|
|
||||||
throw new UnprocessableEntityException(Msg.code(15) + theFieldName + " must be in the form \"{Resource Type}?[params]\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<IBaseResource> findSubscriptionTopicByUrl(String theCriteria) {
|
private Optional<IBaseResource> findSubscriptionTopicByUrl(String theCriteria) {
|
||||||
|
@ -332,6 +301,7 @@ public class SubscriptionValidatingInterceptor {
|
||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
public void setSubscriptionStrategyEvaluatorForUnitTest(SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
|
public void setSubscriptionStrategyEvaluatorForUnitTest(SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
|
||||||
mySubscriptionStrategyEvaluator = theSubscriptionStrategyEvaluator;
|
mySubscriptionStrategyEvaluator = theSubscriptionStrategyEvaluator;
|
||||||
|
mySubscriptionQueryValidator = new SubscriptionQueryValidator(myDaoRegistry, theSubscriptionStrategyEvaluator);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,4 +63,8 @@ public class ActiveSubscriptionTopicCache {
|
||||||
public Collection<SubscriptionTopic> getAll() {
|
public Collection<SubscriptionTopic> getAll() {
|
||||||
return myCache.values();
|
return myCache.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void remove(String theSubscriptionTopicId) {
|
||||||
|
myCache.remove(theSubscriptionTopicId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR Subscription Server
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* 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%
|
||||||
|
*/
|
||||||
|
package ca.uhn.fhir.jpa.topic;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
||||||
|
|
||||||
|
public final class SubscriptionTopicCanonicalizer {
|
||||||
|
private static final FhirContext ourFhirContextR5 = FhirContext.forR5();
|
||||||
|
|
||||||
|
private SubscriptionTopicCanonicalizer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// WIP STR5 use elsewhere
|
||||||
|
public static SubscriptionTopic canonicalize(FhirContext theFhirContext, IBaseResource theSubscriptionTopic) {
|
||||||
|
switch (theFhirContext.getVersion().getVersion()) {
|
||||||
|
case R4B:
|
||||||
|
String encoded = theFhirContext.newJsonParser().encodeResourceToString(theSubscriptionTopic);
|
||||||
|
return ourFhirContextR5.newJsonParser().parseResource(SubscriptionTopic.class, encoded);
|
||||||
|
case R5:
|
||||||
|
return (SubscriptionTopic) theSubscriptionTopic;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException(Msg.code(2337) + "Subscription topics are not supported in FHIR version " + theFhirContext.getVersion().getVersion());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,33 +20,52 @@
|
||||||
package ca.uhn.fhir.jpa.topic;
|
package ca.uhn.fhir.jpa.topic;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelRegistry;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionMatchDeliverer;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
public class SubscriptionTopicConfig {
|
public class SubscriptionTopicConfig {
|
||||||
@Bean
|
@Bean
|
||||||
public SubscriptionTopicMatchingSubscriber subscriptionTopicMatchingSubscriber(FhirContext theFhirContext) {
|
SubscriptionMatchDeliverer subscriptionMatchDeliverer(FhirContext theFhirContext, IInterceptorBroadcaster theInterceptorBroadcaster, SubscriptionChannelRegistry theSubscriptionChannelRegistry) {
|
||||||
|
return new SubscriptionMatchDeliverer(theFhirContext, theInterceptorBroadcaster, theSubscriptionChannelRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SubscriptionTopicMatchingSubscriber subscriptionTopicMatchingSubscriber(FhirContext theFhirContext) {
|
||||||
return new SubscriptionTopicMatchingSubscriber(theFhirContext);
|
return new SubscriptionTopicMatchingSubscriber(theFhirContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SubscriptionTopicPayloadBuilder subscriptionTopicPayloadBuilder(FhirContext theFhirContext) {
|
SubscriptionTopicPayloadBuilder subscriptionTopicPayloadBuilder(FhirContext theFhirContext) {
|
||||||
return new SubscriptionTopicPayloadBuilder(theFhirContext);
|
return new SubscriptionTopicPayloadBuilder(theFhirContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SubscriptionTopicRegistry subscriptionTopicRegistry() {
|
SubscriptionTopicRegistry subscriptionTopicRegistry() {
|
||||||
return new SubscriptionTopicRegistry();
|
return new SubscriptionTopicRegistry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SubscriptionTopicSupport subscriptionTopicSupport(FhirContext theFhirContext, DaoRegistry theDaoRegistry, SearchParamMatcher theSearchParamMatcher) {
|
SubscriptionTopicSupport subscriptionTopicSupport(FhirContext theFhirContext, DaoRegistry theDaoRegistry, SearchParamMatcher theSearchParamMatcher) {
|
||||||
return new SubscriptionTopicSupport(theFhirContext, theDaoRegistry, theSearchParamMatcher);
|
return new SubscriptionTopicSupport(theFhirContext, theDaoRegistry, theSearchParamMatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SubscriptionTopicLoader subscriptionTopicLoader() {
|
SubscriptionTopicLoader subscriptionTopicLoader() {
|
||||||
return new SubscriptionTopicLoader();
|
return new SubscriptionTopicLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SubscriptionTopicRegisteringSubscriber subscriptionTopicRegisteringSubscriber() {
|
||||||
|
return new SubscriptionTopicRegisteringSubscriber();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SubscriptionTopicValidatingInterceptor subscriptionTopicValidatingInterceptor(FhirContext theFhirContext, SubscriptionQueryValidator theSubscriptionQueryValidator) {
|
||||||
|
return new SubscriptionTopicValidatingInterceptor(theFhirContext, theSubscriptionQueryValidator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,12 +45,11 @@ public class SubscriptionTopicMatcher {
|
||||||
SubscriptionTriggerMatcher matcher = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, theMsg, next);
|
SubscriptionTriggerMatcher matcher = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, theMsg, next);
|
||||||
InMemoryMatchResult result = matcher.match();
|
InMemoryMatchResult result = matcher.match();
|
||||||
if (result.matched()) {
|
if (result.matched()) {
|
||||||
|
// as soon as one trigger matches, we're done
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
// WIP STR5 should we check the other triggers?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// WIP STR5 add support for event triggers
|
|
||||||
return InMemoryMatchResult.noMatch();
|
return InMemoryMatchResult.noMatch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
|
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -41,6 +42,7 @@ import org.springframework.messaging.MessagingException;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class SubscriptionTopicMatchingSubscriber implements MessageHandler {
|
public class SubscriptionTopicMatchingSubscriber implements MessageHandler {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicMatchingSubscriber.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicMatchingSubscriber.class);
|
||||||
|
@ -95,21 +97,23 @@ public class SubscriptionTopicMatchingSubscriber implements MessageHandler {
|
||||||
SubscriptionTopicMatcher matcher = new SubscriptionTopicMatcher(mySubscriptionTopicSupport, topic);
|
SubscriptionTopicMatcher matcher = new SubscriptionTopicMatcher(mySubscriptionTopicSupport, topic);
|
||||||
InMemoryMatchResult result = matcher.match(theMsg);
|
InMemoryMatchResult result = matcher.match(theMsg);
|
||||||
if (result.matched()) {
|
if (result.matched()) {
|
||||||
ourLog.info("Matched topic {} to message {}", topic.getIdElement().toUnqualifiedVersionless(), theMsg);
|
ourLog.info("Matched topic {} to message {}", topic.getUrl(), theMsg);
|
||||||
deliverToTopicSubscriptions(theMsg, topic, result);
|
deliverToTopicSubscriptions(theMsg, topic, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deliverToTopicSubscriptions(ResourceModifiedMessage theMsg, SubscriptionTopic topic, InMemoryMatchResult result) {
|
private void deliverToTopicSubscriptions(ResourceModifiedMessage theMsg, SubscriptionTopic topic, InMemoryMatchResult result) {
|
||||||
List<ActiveSubscription> topicSubscriptions = mySubscriptionRegistry.getTopicSubscriptionsByUrl(topic.getUrl());
|
List<ActiveSubscription> topicSubscriptions = mySubscriptionRegistry.getTopicSubscriptionsByTopic(topic.getUrl());
|
||||||
if (!topicSubscriptions.isEmpty()) {
|
if (!topicSubscriptions.isEmpty()) {
|
||||||
IBaseResource matchedResource = theMsg.getNewPayload(myFhirContext);
|
IBaseResource matchedResource = theMsg.getNewPayload(myFhirContext);
|
||||||
|
|
||||||
for (ActiveSubscription activeSubscription : topicSubscriptions) {
|
for (ActiveSubscription activeSubscription : topicSubscriptions) {
|
||||||
// WIP STR5 apply subscription filter
|
// WIP STR5 apply subscription filters
|
||||||
IBaseResource payload = mySubscriptionTopicPayloadBuilder.buildPayload(matchedResource, theMsg, activeSubscription, topic);
|
IBaseBundle bundlePayload = mySubscriptionTopicPayloadBuilder.buildPayload(matchedResource, theMsg, activeSubscription, topic);
|
||||||
mySubscriptionMatchDeliverer.deliverPayload(payload, theMsg, activeSubscription, result);
|
// WIP STR5 do we need to add a total? If so can do that with R5BundleFactory
|
||||||
|
bundlePayload.setId(UUID.randomUUID().toString());
|
||||||
|
mySubscriptionMatchDeliverer.deliverPayload(bundlePayload, theMsg, activeSubscription, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import ca.uhn.fhir.i18n.Msg;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription;
|
import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
||||||
import ca.uhn.fhir.util.BundleBuilder;
|
import ca.uhn.fhir.util.BundleBuilder;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.r5.model.Bundle;
|
import org.hl7.fhir.r5.model.Bundle;
|
||||||
import org.hl7.fhir.r5.model.Enumerations;
|
import org.hl7.fhir.r5.model.Enumerations;
|
||||||
|
@ -32,6 +33,8 @@ import org.hl7.fhir.r5.model.Reference;
|
||||||
import org.hl7.fhir.r5.model.SubscriptionStatus;
|
import org.hl7.fhir.r5.model.SubscriptionStatus;
|
||||||
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class SubscriptionTopicPayloadBuilder {
|
public class SubscriptionTopicPayloadBuilder {
|
||||||
private final FhirContext myFhirContext;
|
private final FhirContext myFhirContext;
|
||||||
|
|
||||||
|
@ -39,7 +42,7 @@ public class SubscriptionTopicPayloadBuilder {
|
||||||
myFhirContext = theFhirContext;
|
myFhirContext = theFhirContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IBaseResource buildPayload(IBaseResource theMatchedResource, ResourceModifiedMessage theMsg, ActiveSubscription theActiveSubscription, SubscriptionTopic theTopic) {
|
public IBaseBundle buildPayload(IBaseResource theMatchedResource, ResourceModifiedMessage theMsg, ActiveSubscription theActiveSubscription, SubscriptionTopic theTopic) {
|
||||||
BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext);
|
BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext);
|
||||||
|
|
||||||
// WIP STR5 set eventsSinceSubscriptionStart from the database
|
// WIP STR5 set eventsSinceSubscriptionStart from the database
|
||||||
|
@ -48,6 +51,8 @@ public class SubscriptionTopicPayloadBuilder {
|
||||||
|
|
||||||
FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion();
|
FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion();
|
||||||
|
|
||||||
|
// WIP STR5 add support for notificationShape include, revinclude
|
||||||
|
|
||||||
if (fhirVersion == FhirVersionEnum.R4B) {
|
if (fhirVersion == FhirVersionEnum.R4B) {
|
||||||
bundleBuilder.setType(Bundle.BundleType.HISTORY.toCode());
|
bundleBuilder.setType(Bundle.BundleType.HISTORY.toCode());
|
||||||
String serializedSubscriptionStatus = FhirContext.forR5Cached().newJsonParser().encodeResourceToString(subscriptionStatus);
|
String serializedSubscriptionStatus = FhirContext.forR5Cached().newJsonParser().encodeResourceToString(subscriptionStatus);
|
||||||
|
@ -60,7 +65,8 @@ public class SubscriptionTopicPayloadBuilder {
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException(Msg.code(2331) + "SubscriptionTopic subscriptions are not supported on FHIR version: " + fhirVersion);
|
throw new IllegalStateException(Msg.code(2331) + "SubscriptionTopic subscriptions are not supported on FHIR version: " + fhirVersion);
|
||||||
}
|
}
|
||||||
// WIP STR5 is this the right type of entry?
|
// WIP STR5 is this the right type of entry? see http://hl7.org/fhir/subscriptionstatus-examples.html
|
||||||
|
// WIP STR5 Also see http://hl7.org/fhir/R4B/notification-full-resource.json.html need to conform to these
|
||||||
bundleBuilder.addCollectionEntry(subscriptionStatus);
|
bundleBuilder.addCollectionEntry(subscriptionStatus);
|
||||||
switch (theMsg.getOperationType()) {
|
switch (theMsg.getOperationType()) {
|
||||||
case CREATE:
|
case CREATE:
|
||||||
|
@ -78,9 +84,11 @@ public class SubscriptionTopicPayloadBuilder {
|
||||||
|
|
||||||
private SubscriptionStatus buildSubscriptionStatus(IBaseResource theMatchedResource, ActiveSubscription theActiveSubscription, SubscriptionTopic theTopic, int theEventsSinceSubscriptionStart) {
|
private SubscriptionStatus buildSubscriptionStatus(IBaseResource theMatchedResource, ActiveSubscription theActiveSubscription, SubscriptionTopic theTopic, int theEventsSinceSubscriptionStart) {
|
||||||
SubscriptionStatus subscriptionStatus = new SubscriptionStatus();
|
SubscriptionStatus subscriptionStatus = new SubscriptionStatus();
|
||||||
|
subscriptionStatus.setId(UUID.randomUUID().toString());
|
||||||
subscriptionStatus.setStatus(Enumerations.SubscriptionStatusCodes.ACTIVE);
|
subscriptionStatus.setStatus(Enumerations.SubscriptionStatusCodes.ACTIVE);
|
||||||
subscriptionStatus.setType(SubscriptionStatus.SubscriptionNotificationType.EVENTNOTIFICATION);
|
subscriptionStatus.setType(SubscriptionStatus.SubscriptionNotificationType.EVENTNOTIFICATION);
|
||||||
// WIP STR5 count events since subscription start and set eventsSinceSubscriptionStart
|
// WIP STR5 count events since subscription start and set eventsSinceSubscriptionStart
|
||||||
|
// store counts by subscription id
|
||||||
subscriptionStatus.setEventsSinceSubscriptionStart(theEventsSinceSubscriptionStart);
|
subscriptionStatus.setEventsSinceSubscriptionStart(theEventsSinceSubscriptionStart);
|
||||||
subscriptionStatus.addNotificationEvent().setEventNumber(theEventsSinceSubscriptionStart).setFocus(new Reference(theMatchedResource.getIdElement()));
|
subscriptionStatus.addNotificationEvent().setEventNumber(theEventsSinceSubscriptionStart).setFocus(new Reference(theMatchedResource.getIdElement()));
|
||||||
subscriptionStatus.setSubscription(new Reference(theActiveSubscription.getSubscription().getIdElement(myFhirContext)));
|
subscriptionStatus.setSubscription(new Reference(theActiveSubscription.getSubscription().getIdElement(myFhirContext)));
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR Subscription Server
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* 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%
|
||||||
|
*/
|
||||||
|
package ca.uhn.fhir.jpa.topic;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||||
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
|
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
import org.hl7.fhir.r5.model.Enumerations;
|
||||||
|
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.messaging.Message;
|
||||||
|
import org.springframework.messaging.MessageHandler;
|
||||||
|
import org.springframework.messaging.MessagingException;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for transitioning subscription resources from REQUESTED to ACTIVE
|
||||||
|
* Once activated, the subscription is added to the SubscriptionRegistry.
|
||||||
|
* <p>
|
||||||
|
* Also validates criteria. If invalid, rejects the subscription without persisting the subscription.
|
||||||
|
*/
|
||||||
|
public class SubscriptionTopicRegisteringSubscriber implements MessageHandler {
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicRegisteringSubscriber.class);
|
||||||
|
@Autowired
|
||||||
|
private FhirContext myFhirContext;
|
||||||
|
@Autowired
|
||||||
|
private SubscriptionTopicRegistry mySubscriptionTopicRegistry;
|
||||||
|
@Autowired
|
||||||
|
private DaoRegistry myDaoRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public SubscriptionTopicRegisteringSubscriber() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(@Nonnull Message<?> theMessage) throws MessagingException {
|
||||||
|
if (!(theMessage instanceof ResourceModifiedJsonMessage)) {
|
||||||
|
ourLog.warn("Received message of unexpected type on matching channel: {}", theMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceModifiedMessage payload = ((ResourceModifiedJsonMessage) theMessage).getPayload();
|
||||||
|
|
||||||
|
if (!payload.hasPayloadType(myFhirContext, "SubscriptionTopic")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (payload.getOperationType()) {
|
||||||
|
case MANUALLY_TRIGGERED:
|
||||||
|
case TRANSACTION:
|
||||||
|
return;
|
||||||
|
case CREATE:
|
||||||
|
case UPDATE:
|
||||||
|
case DELETE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We read the resource back from the DB instead of using the supplied copy for
|
||||||
|
// two reasons:
|
||||||
|
// - in order to store partition id in the userdata of the resource for partitioned subscriptions
|
||||||
|
// - in case we're processing out of order and a create-then-delete has been processed backwards (or vice versa)
|
||||||
|
|
||||||
|
IBaseResource payloadResource;
|
||||||
|
IIdType payloadId = payload.getPayloadId(myFhirContext).toUnqualifiedVersionless();
|
||||||
|
try {
|
||||||
|
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getResourceDao("SubscriptionTopic");
|
||||||
|
RequestDetails systemRequestDetails = getPartitionAwareRequestDetails(payload);
|
||||||
|
payloadResource = subscriptionDao.read(payloadId, systemRequestDetails);
|
||||||
|
if (payloadResource == null) {
|
||||||
|
// Only for unit test
|
||||||
|
payloadResource = payload.getPayload(myFhirContext);
|
||||||
|
}
|
||||||
|
} catch (ResourceGoneException e) {
|
||||||
|
mySubscriptionTopicRegistry.unregister(payloadId.getIdPart());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscriptionTopic subscriptionTopic = SubscriptionTopicCanonicalizer.canonicalize(myFhirContext, payloadResource);
|
||||||
|
if (subscriptionTopic.getStatus() == Enumerations.PublicationStatus.ACTIVE) {
|
||||||
|
mySubscriptionTopicRegistry.register(subscriptionTopic);
|
||||||
|
} else {
|
||||||
|
mySubscriptionTopicRegistry.unregister(payloadId.getIdPart());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There were some situations where the RequestDetails attempted to use the default partition
|
||||||
|
* and the partition name was a list containing null values (i.e. using the package installer to STORE_AND_INSTALL
|
||||||
|
* Subscriptions while partitioning was enabled). If any partition matches these criteria,
|
||||||
|
* {@link RequestPartitionId#defaultPartition()} is used to obtain the default partition.
|
||||||
|
*/
|
||||||
|
private RequestDetails getPartitionAwareRequestDetails(ResourceModifiedMessage payload) {
|
||||||
|
RequestPartitionId payloadPartitionId = payload.getPartitionId();
|
||||||
|
if (payloadPartitionId == null || payloadPartitionId.isDefaultPartition()) {
|
||||||
|
// This may look redundant but the package installer STORE_AND_INSTALL Subscriptions when partitioning is enabled
|
||||||
|
// creates a corrupt default partition. This resets it to a clean one.
|
||||||
|
payloadPartitionId = RequestPartitionId.defaultPartition();
|
||||||
|
}
|
||||||
|
return new SystemRequestDetails().setRequestPartitionId(payloadPartitionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -45,4 +45,8 @@ public class SubscriptionTopicRegistry {
|
||||||
public Collection<SubscriptionTopic> getAll() {
|
public Collection<SubscriptionTopic> getAll() {
|
||||||
return myActiveSubscriptionTopicCache.getAll();
|
return myActiveSubscriptionTopicCache.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void unregister(String theSubscriptionTopicId) {
|
||||||
|
myActiveSubscriptionTopicCache.remove(theSubscriptionTopicId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR Subscription Server
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* 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%
|
||||||
|
*/
|
||||||
|
package ca.uhn.fhir.jpa.topic;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator;
|
||||||
|
import ca.uhn.fhir.parser.DataFormatException;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class SubscriptionTopicValidatingInterceptor {
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicValidatingInterceptor.class);
|
||||||
|
private final FhirContext myFhirContext;
|
||||||
|
private final SubscriptionQueryValidator mySubscriptionQueryValidator;
|
||||||
|
|
||||||
|
public SubscriptionTopicValidatingInterceptor(FhirContext theFhirContext, SubscriptionQueryValidator theSubscriptionQueryValidator) {
|
||||||
|
myFhirContext = theFhirContext;
|
||||||
|
mySubscriptionQueryValidator = theSubscriptionQueryValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
|
||||||
|
public void resourcePreCreate(IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
|
||||||
|
validateSubmittedSubscriptionTopic(theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
|
||||||
|
public void resourceUpdated(IBaseResource theOldResource, IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
|
||||||
|
validateSubmittedSubscriptionTopic(theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void validateSubmittedSubscriptionTopic(IBaseResource theSubscription,
|
||||||
|
RequestDetails theRequestDetails,
|
||||||
|
RequestPartitionId theRequestPartitionId,
|
||||||
|
Pointcut thePointcut) {
|
||||||
|
if (Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED != thePointcut && Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED != thePointcut) {
|
||||||
|
throw new UnprocessableEntityException(Msg.code(2340) + "Expected Pointcut to be either STORAGE_PRESTORAGE_RESOURCE_CREATED or STORAGE_PRESTORAGE_RESOURCE_UPDATED but was: " + thePointcut);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"SubscriptionTopic".equals(myFhirContext.getResourceType(theSubscription))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscriptionTopic subscriptionTopic = SubscriptionTopicCanonicalizer.canonicalize(myFhirContext, theSubscription);
|
||||||
|
|
||||||
|
boolean finished = false;
|
||||||
|
if (subscriptionTopic.getStatus() == null) {
|
||||||
|
throw new UnprocessableEntityException(Msg.code(2338) + "Can not process submitted SubscriptionTopic - SubscriptionTopic.status must be populated on this server");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (subscriptionTopic.getStatus()) {
|
||||||
|
case ACTIVE:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
finished = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WIP STR5 add cross-partition support like in SubscriptionValidatingInterceptor
|
||||||
|
|
||||||
|
// WIP STR5 warn if can't be evaluated in memory?
|
||||||
|
|
||||||
|
if (!finished) {
|
||||||
|
subscriptionTopic.getResourceTrigger().stream()
|
||||||
|
.forEach(t -> validateQueryCriteria(t.getQueryCriteria()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateQueryCriteria(SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria) {
|
||||||
|
if (theQueryCriteria.getPrevious() != null) {
|
||||||
|
validateCriteria(theQueryCriteria.getPrevious(), "SubscriptionTopic.resourceTrigger.queryCriteria.previous");
|
||||||
|
}
|
||||||
|
if (theQueryCriteria.getCurrent() != null) {
|
||||||
|
validateCriteria(theQueryCriteria.getCurrent(), "SubscriptionTopic.resourceTrigger.queryCriteria.current");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateCriteria(String theCriteria, String theFieldName) {
|
||||||
|
try {
|
||||||
|
mySubscriptionQueryValidator.validateCriteria(theCriteria, theFieldName);
|
||||||
|
SubscriptionMatchingStrategy strategy = mySubscriptionQueryValidator.determineStrategy(theCriteria);
|
||||||
|
if (strategy != SubscriptionMatchingStrategy.IN_MEMORY) {
|
||||||
|
ourLog.warn("Warning: Query Criteria '{}' in {} cannot be evaluated in-memory", theCriteria, theFieldName);
|
||||||
|
}
|
||||||
|
} catch (InvalidRequestException | DataFormatException e) {
|
||||||
|
throw new UnprocessableEntityException(Msg.code(2339) + "Invalid SubscriptionTopic criteria '" + theCriteria + "' in " + theFieldName + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,10 +66,18 @@ public class SubscriptionTriggerMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
private InMemoryMatchResult match(SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria) {
|
private InMemoryMatchResult match(SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria) {
|
||||||
InMemoryMatchResult previousMatches = InMemoryMatchResult.successfulMatch();
|
|
||||||
InMemoryMatchResult currentMatches = InMemoryMatchResult.successfulMatch();
|
|
||||||
String previousCriteria = theQueryCriteria.getPrevious();
|
String previousCriteria = theQueryCriteria.getPrevious();
|
||||||
String currentCriteria = theQueryCriteria.getCurrent();
|
String currentCriteria = theQueryCriteria.getCurrent();
|
||||||
|
InMemoryMatchResult previousMatches = InMemoryMatchResult.fromBoolean(previousCriteria == null);
|
||||||
|
InMemoryMatchResult currentMatches = InMemoryMatchResult.fromBoolean(currentCriteria == null);
|
||||||
|
|
||||||
|
// WIP STR5 implement fhirPathCriteria per https://build.fhir.org/subscriptiontopic.html#fhirpath-criteria
|
||||||
|
if (currentCriteria != null) {
|
||||||
|
currentMatches = matchResource(myResource, currentCriteria);
|
||||||
|
}
|
||||||
|
if (myOperation == ResourceModifiedMessage.OperationTypeEnum.CREATE) {
|
||||||
|
return currentMatches;
|
||||||
|
}
|
||||||
|
|
||||||
if (previousCriteria != null) {
|
if (previousCriteria != null) {
|
||||||
if (myOperation == ResourceModifiedMessage.OperationTypeEnum.UPDATE ||
|
if (myOperation == ResourceModifiedMessage.OperationTypeEnum.UPDATE ||
|
||||||
|
@ -85,10 +93,7 @@ public class SubscriptionTriggerMatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentCriteria != null) {
|
// WIP STR5 implement resultForCreate and resultForDelete
|
||||||
currentMatches = matchResource(myResource, currentCriteria);
|
|
||||||
}
|
|
||||||
// WIP STR5 is this the correct interpretation of requireBoth?
|
|
||||||
if (theQueryCriteria.getRequireBoth()) {
|
if (theQueryCriteria.getRequireBoth()) {
|
||||||
return InMemoryMatchResult.and(previousMatches, currentMatches);
|
return InMemoryMatchResult.and(previousMatches, currentMatches);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
|
||||||
import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory;
|
import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
|
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
|
||||||
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
|
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -49,6 +50,8 @@ public class DaoSubscriptionMatcherTest {
|
||||||
private IValidationSupport myValidationSupport;
|
private IValidationSupport myValidationSupport;
|
||||||
@MockBean
|
@MockBean
|
||||||
private SubscriptionChannelFactory mySubscriptionChannelFactory;
|
private SubscriptionChannelFactory mySubscriptionChannelFactory;
|
||||||
|
@MockBean
|
||||||
|
private SubscriptionQueryValidator mySubscriptionQueryValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make sure that if we're only running the {@link SubscriptionSubmitterConfig}, we don't need
|
* Make sure that if we're only running the {@link SubscriptionSubmitterConfig}, we don't need
|
||||||
|
|
|
@ -113,22 +113,22 @@ public class ActiveSubscriptionCacheTest {
|
||||||
ActiveSubscriptionCache activeSubscriptionCache = new ActiveSubscriptionCache();
|
ActiveSubscriptionCache activeSubscriptionCache = new ActiveSubscriptionCache();
|
||||||
ActiveSubscription activeSub1 = buildActiveSubscription(ID1);
|
ActiveSubscription activeSub1 = buildActiveSubscription(ID1);
|
||||||
activeSubscriptionCache.put(ID1, activeSub1);
|
activeSubscriptionCache.put(ID1, activeSub1);
|
||||||
assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL), hasSize(0));
|
assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL), hasSize(0));
|
||||||
|
|
||||||
ActiveSubscription activeSub2 = buildTopicSubscription(ID2, TEST_TOPIC_URL);
|
ActiveSubscription activeSub2 = buildTopicSubscription(ID2, TEST_TOPIC_URL);
|
||||||
activeSubscriptionCache.put(ID2, activeSub2);
|
activeSubscriptionCache.put(ID2, activeSub2);
|
||||||
assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL), hasSize(1));
|
assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL), hasSize(1));
|
||||||
ActiveSubscription match = activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL).get(0);
|
ActiveSubscription match = activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL).get(0);
|
||||||
assertEquals(ID2, match.getId());
|
assertEquals(ID2, match.getId());
|
||||||
|
|
||||||
ActiveSubscription activeSub3 = buildTopicSubscription(ID3, TEST_TOPIC_URL_OTHER);
|
ActiveSubscription activeSub3 = buildTopicSubscription(ID3, TEST_TOPIC_URL_OTHER);
|
||||||
activeSubscriptionCache.put(ID3, activeSub3);
|
activeSubscriptionCache.put(ID3, activeSub3);
|
||||||
assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL), hasSize(1));
|
assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL), hasSize(1));
|
||||||
match = activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL).get(0);
|
match = activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL).get(0);
|
||||||
assertEquals(ID2, match.getId());
|
assertEquals(ID2, match.getId());
|
||||||
|
|
||||||
assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL_OTHER), hasSize(1));
|
assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL_OTHER), hasSize(1));
|
||||||
match = activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL_OTHER).get(0);
|
match = activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL_OTHER).get(0);
|
||||||
assertEquals(ID3, match.getId());
|
assertEquals(ID3, match.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ public class ActiveSubscriptionCacheTest {
|
||||||
private ActiveSubscription buildTopicSubscription(String theId, String theTopicUrl) {
|
private ActiveSubscription buildTopicSubscription(String theId, String theTopicUrl) {
|
||||||
ActiveSubscription activeSub2 = buildActiveSubscription(theId);
|
ActiveSubscription activeSub2 = buildActiveSubscription(theId);
|
||||||
activeSub2.getSubscription().setTopicSubscription(true);
|
activeSub2.getSubscription().setTopicSubscription(true);
|
||||||
activeSub2.getSubscription().setCriteriaString(theTopicUrl);
|
activeSub2.getSubscription().getTopicSubscription().setTopic(theTopicUrl);
|
||||||
return activeSub2;
|
return activeSub2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -450,7 +450,7 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
|
||||||
when(myInterceptorBroadcaster.callHooks(
|
when(myInterceptorBroadcaster.callHooks(
|
||||||
eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true);
|
eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true);
|
||||||
when(message.getPayloadId(null)).thenReturn(new IdDt("Patient", 123L));
|
when(message.getPayloadId(null)).thenReturn(new IdDt("Patient", 123L));
|
||||||
when(mySubscriptionRegistry.getAll()).thenReturn(Collections.singletonList(myActiveSubscription));
|
when(mySubscriptionRegistry.getAllNonTopicSubscriptions()).thenReturn(Collections.singletonList(myActiveSubscription));
|
||||||
when(myActiveSubscription.getSubscription()).thenReturn(myCanonicalSubscription);
|
when(myActiveSubscription.getSubscription()).thenReturn(myCanonicalSubscription);
|
||||||
when(myActiveSubscription.getCriteria()).thenReturn(mySubscriptionCriteria);
|
when(myActiveSubscription.getCriteria()).thenReturn(mySubscriptionCriteria);
|
||||||
when(myActiveSubscription.getId()).thenReturn("Patient/123");
|
when(myActiveSubscription.getId()).thenReturn("Patient/123");
|
||||||
|
@ -468,7 +468,7 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
|
||||||
eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true);
|
eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true);
|
||||||
when(message.getPayloadId(null)).thenReturn(new IdDt("Patient", 123L));
|
when(message.getPayloadId(null)).thenReturn(new IdDt("Patient", 123L));
|
||||||
when(myNonDeleteCanonicalSubscription.getSendDeleteMessages()).thenReturn(false);
|
when(myNonDeleteCanonicalSubscription.getSendDeleteMessages()).thenReturn(false);
|
||||||
when(mySubscriptionRegistry.getAll()).thenReturn(List.of(myNonDeleteSubscription, myActiveSubscription));
|
when(mySubscriptionRegistry.getAllNonTopicSubscriptions()).thenReturn(List.of(myNonDeleteSubscription, myActiveSubscription));
|
||||||
when(myActiveSubscription.getSubscription()).thenReturn(myCanonicalSubscription);
|
when(myActiveSubscription.getSubscription()).thenReturn(myCanonicalSubscription);
|
||||||
when(myActiveSubscription.getCriteria()).thenReturn(mySubscriptionCriteria);
|
when(myActiveSubscription.getCriteria()).thenReturn(mySubscriptionCriteria);
|
||||||
when(myActiveSubscription.getId()).thenReturn("Patient/123");
|
when(myActiveSubscription.getId()).thenReturn("Patient/123");
|
||||||
|
@ -489,7 +489,7 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
|
||||||
when(myInterceptorBroadcaster.callHooks(
|
when(myInterceptorBroadcaster.callHooks(
|
||||||
eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true);
|
eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true);
|
||||||
when(message.getPayloadId(null)).thenReturn(new IdDt("Patient", 123L));
|
when(message.getPayloadId(null)).thenReturn(new IdDt("Patient", 123L));
|
||||||
when(mySubscriptionRegistry.getAll()).thenReturn(Collections.singletonList(myActiveSubscription));
|
when(mySubscriptionRegistry.getAllNonTopicSubscriptions()).thenReturn(Collections.singletonList(myActiveSubscription));
|
||||||
when(myActiveSubscription.getSubscription()).thenReturn(myCanonicalSubscription);
|
when(myActiveSubscription.getSubscription()).thenReturn(myCanonicalSubscription);
|
||||||
when(myActiveSubscription.getCriteria()).thenReturn(mySubscriptionCriteria);
|
when(myActiveSubscription.getCriteria()).thenReturn(mySubscriptionCriteria);
|
||||||
when(myActiveSubscription.getId()).thenReturn("Patient/123");
|
when(myActiveSubscription.getId()).thenReturn("Patient/123");
|
||||||
|
|
|
@ -202,7 +202,7 @@ public class SubscriptionValidatingInterceptorTest {
|
||||||
mySubscriptionValidatingInterceptor.validateSubmittedSubscription(badSub, null, null, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED);
|
mySubscriptionValidatingInterceptor.validateSubmittedSubscription(badSub, null, null, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED);
|
||||||
fail();
|
fail();
|
||||||
} catch (UnprocessableEntityException e) {
|
} catch (UnprocessableEntityException e) {
|
||||||
assertThat(e.getMessage(), is(Msg.code(2322) + "No SubscriptionTopic exists with url: http://topic.url"));
|
assertThat(e.getMessage(), is(Msg.code(2322) + "No SubscriptionTopic exists with topic: http://topic.url"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Happy path
|
// Happy path
|
||||||
|
@ -228,6 +228,11 @@ public class SubscriptionValidatingInterceptorTest {
|
||||||
SubscriptionCanonicalizer subscriptionCanonicalizer(FhirContext theFhirContext) {
|
SubscriptionCanonicalizer subscriptionCanonicalizer(FhirContext theFhirContext) {
|
||||||
return new SubscriptionCanonicalizer(theFhirContext);
|
return new SubscriptionCanonicalizer(theFhirContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SubscriptionQueryValidator subscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
|
||||||
|
return new SubscriptionQueryValidator(theDaoRegistry, theSubscriptionStrategyEvaluator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
|
|
@ -77,8 +77,7 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
public void testReuseSubscriptionIdWithDifferentDatabaseMode() throws Exception {
|
public void testReuseSubscriptionIdWithDifferentDatabaseMode() throws Exception {
|
||||||
myStorageSettings.setTagStorageMode(JpaStorageSettings.TagStorageModeEnum.NON_VERSIONED);
|
myStorageSettings.setTagStorageMode(JpaStorageSettings.TagStorageModeEnum.NON_VERSIONED);
|
||||||
|
|
||||||
String payload = "application/fhir+json";
|
IdType id = createSubscription("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER", Constants.CT_FHIR_JSON_NEW, null, "sub").getIdElement().toUnqualifiedVersionless();
|
||||||
IdType id = createSubscription("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER", payload, null, "sub").getIdElement().toUnqualifiedVersionless();
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
Subscription subscription = mySubscriptionDao.read(id, mySrd);
|
Subscription subscription = mySubscriptionDao.read(id, mySrd);
|
||||||
|
@ -88,8 +87,7 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
mySubscriptionDao.delete(id, mySrd);
|
mySubscriptionDao.delete(id, mySrd);
|
||||||
waitForActivatedSubscriptionCount(0);
|
waitForActivatedSubscriptionCount(0);
|
||||||
|
|
||||||
payload = "application/fhir+json";
|
id = createSubscription("Observation?", Constants.CT_FHIR_JSON_NEW, null, "sub").getIdElement().toUnqualifiedVersionless();
|
||||||
id = createSubscription("Observation?", payload, null, "sub").getIdElement().toUnqualifiedVersionless();
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
subscription = mySubscriptionDao.read(id, mySrd);
|
subscription = mySubscriptionDao.read(id, mySrd);
|
||||||
|
@ -104,8 +102,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=json";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=json";
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=json";
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=json";
|
||||||
|
|
||||||
createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
createSubscription(criteria1);
|
||||||
createSubscription(criteria2, Constants.CT_FHIR_JSON_NEW);
|
createSubscription(criteria2);
|
||||||
waitForActivatedSubscriptionCount(2);
|
waitForActivatedSubscriptionCount(2);
|
||||||
|
|
||||||
sendObservation(code, "SNOMED-CT");
|
sendObservation(code, "SNOMED-CT");
|
||||||
|
@ -120,12 +118,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdatesHaveCorrectMetadata() throws Exception {
|
public void testUpdatesHaveCorrectMetadata() throws Exception {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?";
|
String criteria1 = "Observation?";
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -171,11 +168,9 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPlaceholderReferencesInTransactionAreResolvedCorrectly() throws Exception {
|
public void testPlaceholderReferencesInTransactionAreResolvedCorrectly() throws Exception {
|
||||||
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?";
|
String criteria1 = "Observation?";
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
// Create a transaction that should match
|
// Create a transaction that should match
|
||||||
|
@ -204,12 +199,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception {
|
public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?";
|
String criteria1 = "Observation?";
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -265,12 +258,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRepeatedDeliveries() throws Exception {
|
public void testRepeatedDeliveries() throws Exception {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?";
|
String criteria1 = "Observation?";
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
for (int i = 0; i < 100; i++) {
|
for (int i = 0; i < 100; i++) {
|
||||||
|
@ -287,12 +278,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSubscriptionRegistryLoadsSubscriptionsFromDatabase() throws Exception {
|
public void testSubscriptionRegistryLoadsSubscriptionsFromDatabase() throws Exception {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?";
|
String criteria1 = "Observation?";
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
// Manually unregister all subscriptions
|
// Manually unregister all subscriptions
|
||||||
|
@ -315,8 +305,7 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
@Test
|
@Test
|
||||||
public void testActiveSubscriptionShouldntReActivate() throws Exception {
|
public void testActiveSubscriptionShouldntReActivate() throws Exception {
|
||||||
String criteria = "Observation?code=111111111&_format=xml";
|
String criteria = "Observation?code=111111111&_format=xml";
|
||||||
String payload = "application/fhir+json";
|
createSubscription(criteria);
|
||||||
createSubscription(criteria, payload);
|
|
||||||
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
|
@ -327,14 +316,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionMetaAddDoesntTriggerNewDelivery() throws Exception {
|
public void testRestHookSubscriptionMetaAddDoesntTriggerNewDelivery() throws Exception {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
createSubscription(criteria2, payload);
|
createSubscription(criteria2);
|
||||||
waitForActivatedSubscriptionCount(2);
|
waitForActivatedSubscriptionCount(2);
|
||||||
|
|
||||||
ourLog.info("Sending an Observation");
|
ourLog.info("Sending an Observation");
|
||||||
|
@ -381,14 +368,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
public void testRestHookSubscriptionMetaAddDoesTriggerNewDeliveryIfConfiguredToDoSo() throws Exception {
|
public void testRestHookSubscriptionMetaAddDoesTriggerNewDeliveryIfConfiguredToDoSo() throws Exception {
|
||||||
myStorageSettings.setTriggerSubscriptionsForNonVersioningChanges(true);
|
myStorageSettings.setTriggerSubscriptionsForNonVersioningChanges(true);
|
||||||
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
createSubscription(criteria2, payload);
|
createSubscription(criteria2);
|
||||||
waitForActivatedSubscriptionCount(2);
|
waitForActivatedSubscriptionCount(2);
|
||||||
|
|
||||||
Observation obs = sendObservation(code, "SNOMED-CT");
|
Observation obs = sendObservation(code, "SNOMED-CT");
|
||||||
|
@ -431,14 +416,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionNoopUpdateDoesntTriggerNewDelivery() throws Exception {
|
public void testRestHookSubscriptionNoopUpdateDoesntTriggerNewDelivery() throws Exception {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
createSubscription(criteria2, payload);
|
createSubscription(criteria2);
|
||||||
waitForActivatedSubscriptionCount(2);
|
waitForActivatedSubscriptionCount(2);
|
||||||
|
|
||||||
Observation obs = sendObservation(code, "SNOMED-CT");
|
Observation obs = sendObservation(code, "SNOMED-CT");
|
||||||
|
@ -464,13 +447,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionApplicationJsonDisableVersionIdInDelivery() throws Exception {
|
public void testRestHookSubscriptionApplicationJsonDisableVersionIdInDelivery() throws Exception {
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
|
|
||||||
waitForActivatedSubscriptionCount(0);
|
waitForActivatedSubscriptionCount(0);
|
||||||
Subscription subscription1 = createSubscription(criteria1, payload);
|
Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
|
||||||
|
@ -512,13 +493,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionDoesntGetLatestVersionByDefault() throws Exception {
|
public void testRestHookSubscriptionDoesntGetLatestVersionByDefault() throws Exception {
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
|
|
||||||
waitForActivatedSubscriptionCount(0);
|
waitForActivatedSubscriptionCount(0);
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
myStoppableSubscriptionDeliveringRestHookSubscriber.pause();
|
myStoppableSubscriptionDeliveringRestHookSubscriber.pause();
|
||||||
|
@ -553,18 +532,20 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
assertEquals("changed", observation2.getNoteFirstRep().getText());
|
assertEquals("changed", observation2.getNoteFirstRep().getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createSubscription(String criteria1) {
|
||||||
|
createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = {"[*]", "[Observation]", "Observation?"})
|
@ValueSource(strings = {"[*]", "[Observation]", "Observation?"})
|
||||||
public void RestHookSubscriptionWithPayloadSendsDeleteRequest(String theCriteria) throws Exception {
|
public void RestHookSubscriptionWithPayloadSendsDeleteRequest(String theCriteria) throws Exception {
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
Extension sendDeleteMessagesExtension = new Extension()
|
Extension sendDeleteMessagesExtension = new Extension()
|
||||||
.setUrl(EX_SEND_DELETE_MESSAGES)
|
.setUrl(EX_SEND_DELETE_MESSAGES)
|
||||||
.setValue(new BooleanType(true));
|
.setValue(new BooleanType(true));
|
||||||
|
|
||||||
waitForActivatedSubscriptionCount(0);
|
waitForActivatedSubscriptionCount(0);
|
||||||
createSubscription(theCriteria, payload, sendDeleteMessagesExtension);
|
createSubscription(theCriteria, Constants.CT_FHIR_JSON_NEW, sendDeleteMessagesExtension);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
ourLog.info("** About to send observation");
|
ourLog.info("** About to send observation");
|
||||||
|
@ -580,14 +561,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionGetsLatestVersionWithFlag() throws Exception {
|
public void testRestHookSubscriptionGetsLatestVersionWithFlag() throws Exception {
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
|
|
||||||
waitForActivatedSubscriptionCount(0);
|
waitForActivatedSubscriptionCount(0);
|
||||||
|
|
||||||
Subscription subscription = newSubscription(criteria1, payload);
|
Subscription subscription = newSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||||
subscription
|
subscription
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true"));
|
.addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true"));
|
||||||
|
@ -629,14 +608,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionApplicationJson() throws Exception {
|
public void testRestHookSubscriptionApplicationJson() throws Exception {
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
Subscription subscription1 = createSubscription(criteria1, payload);
|
Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||||
Subscription subscription2 = createSubscription(criteria2, payload);
|
Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_JSON_NEW);
|
||||||
waitForActivatedSubscriptionCount(2);
|
waitForActivatedSubscriptionCount(2);
|
||||||
|
|
||||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
||||||
|
@ -709,14 +686,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
public void testRestHookSubscriptionApplicationJsonDatabase() throws Exception {
|
public void testRestHookSubscriptionApplicationJsonDatabase() throws Exception {
|
||||||
// Same test as above, but now run it using database matching
|
// Same test as above, but now run it using database matching
|
||||||
myStorageSettings.setEnableInMemorySubscriptionMatching(false);
|
myStorageSettings.setEnableInMemorySubscriptionMatching(false);
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
Subscription subscription1 = createSubscription(criteria1, payload);
|
Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||||
Subscription subscription2 = createSubscription(criteria2, payload);
|
Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_JSON_NEW);
|
||||||
waitForActivatedSubscriptionCount(2);
|
waitForActivatedSubscriptionCount(2);
|
||||||
|
|
||||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
||||||
|
@ -787,14 +762,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionApplicationXml() throws Exception {
|
public void testRestHookSubscriptionApplicationXml() throws Exception {
|
||||||
String payload = "application/xml";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
Subscription subscription1 = createSubscription(criteria1, payload);
|
Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_XML_NEW);
|
||||||
Subscription subscription2 = createSubscription(criteria2, payload);
|
Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_XML_NEW);
|
||||||
waitForActivatedSubscriptionCount(2);
|
waitForActivatedSubscriptionCount(2);
|
||||||
|
|
||||||
ourLog.info("** About to send observation");
|
ourLog.info("** About to send observation");
|
||||||
|
@ -861,12 +834,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionStarCriteria() throws Exception {
|
public void testRestHookSubscriptionStarCriteria() throws Exception {
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "[*]";
|
String criteria1 = "[*]";
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
sendObservation(code, "SNOMED-CT");
|
sendObservation(code, "SNOMED-CT");
|
||||||
|
@ -887,12 +858,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionMultiTypeCriteria() throws Exception {
|
public void testRestHookSubscriptionMultiTypeCriteria() throws Exception {
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "[Observation,Patient]";
|
String criteria1 = "[Observation,Patient]";
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
sendOrganization();
|
sendOrganization();
|
||||||
|
@ -915,12 +884,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSubscriptionTriggerViaSubscription() throws Exception {
|
public void testSubscriptionTriggerViaSubscription() throws Exception {
|
||||||
String payload = "application/xml";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1, Constants.CT_FHIR_XML_NEW);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
ourLog.info("** About to send observation");
|
ourLog.info("** About to send observation");
|
||||||
|
@ -966,14 +933,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdateSubscriptionToMatchLater() throws Exception {
|
public void testUpdateSubscriptionToMatchLater() throws Exception {
|
||||||
String payload = "application/xml";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteriaBad = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
String criteriaBad = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
ourLog.info("** About to create non-matching subscription");
|
ourLog.info("** About to create non-matching subscription");
|
||||||
|
|
||||||
Subscription subscription2 = createSubscription(criteriaBad, payload);
|
Subscription subscription2 = createSubscription(criteriaBad, Constants.CT_FHIR_XML_NEW);
|
||||||
|
|
||||||
ourLog.info("** About to send observation that wont match");
|
ourLog.info("** About to send observation that wont match");
|
||||||
|
|
||||||
|
@ -1011,14 +976,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionApplicationXmlJson() throws Exception {
|
public void testRestHookSubscriptionApplicationXmlJson() throws Exception {
|
||||||
String payload = "application/fhir+xml";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
||||||
|
|
||||||
Subscription subscription1 = createSubscription(criteria1, payload);
|
Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_XML_NEW);
|
||||||
Subscription subscription2 = createSubscription(criteria2, payload);
|
Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_XML_NEW);
|
||||||
waitForActivatedSubscriptionCount(2);
|
waitForActivatedSubscriptionCount(2);
|
||||||
|
|
||||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
||||||
|
@ -1032,12 +995,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRestHookSubscriptionInvalidCriteria() throws Exception {
|
public void testRestHookSubscriptionInvalidCriteria() throws Exception {
|
||||||
String payload = "application/xml";
|
|
||||||
|
|
||||||
String criteria1 = "Observation?codeeeee=SNOMED-CT";
|
String criteria1 = "Observation?codeeeee=SNOMED-CT";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
createSubscription(criteria1, payload);
|
createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||||
fail();
|
fail();
|
||||||
} catch (UnprocessableEntityException e) {
|
} catch (UnprocessableEntityException e) {
|
||||||
assertEquals("HTTP 422 Unprocessable Entity: " + Msg.code(9) + "Invalid subscription criteria submitted: Observation?codeeeee=SNOMED-CT " + Msg.code(488) + "Failed to parse match URL[Observation?codeeeee=SNOMED-CT] - Resource type Observation does not have a parameter with name: codeeeee", e.getMessage());
|
assertEquals("HTTP 422 Unprocessable Entity: " + Msg.code(9) + "Invalid subscription criteria submitted: Observation?codeeeee=SNOMED-CT " + Msg.code(488) + "Failed to parse match URL[Observation?codeeeee=SNOMED-CT] - Resource type Observation does not have a parameter with name: codeeeee", e.getMessage());
|
||||||
|
@ -1046,13 +1007,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSubscriptionWithHeaders() throws Exception {
|
public void testSubscriptionWithHeaders() throws Exception {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
|
|
||||||
// Add some headers, and we'll also turn back to requested status for fun
|
// Add some headers, and we'll also turn back to requested status for fun
|
||||||
Subscription subscription = createSubscription(criteria1, payload);
|
Subscription subscription = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
subscription.getChannel().addHeader("X-Foo: FOO");
|
subscription.getChannel().addHeader("X-Foo: FOO");
|
||||||
|
@ -1074,12 +1033,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDisableSubscription() throws Exception {
|
public void testDisableSubscription() throws Exception {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
String code = "1000000050";
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
||||||
|
|
||||||
Subscription subscription = createSubscription(criteria1, payload);
|
Subscription subscription = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
sendObservation(code, "SNOMED-CT");
|
sendObservation(code, "SNOMED-CT");
|
||||||
|
@ -1107,9 +1064,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
@Test
|
@Test
|
||||||
public void testInvalidProvenanceParam() {
|
public void testInvalidProvenanceParam() {
|
||||||
assertThrows(UnprocessableEntityException.class, () -> {
|
assertThrows(UnprocessableEntityException.class, () -> {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String criteriabad = "Provenance?activity=http://hl7.org/fhir/v3/DocumentCompletion%7CAU";
|
String criteriabad = "Provenance?activity=http://hl7.org/fhir/v3/DocumentCompletion%7CAU";
|
||||||
Subscription subscription = newSubscription(criteriabad, payload);
|
Subscription subscription = newSubscription(criteriabad, Constants.CT_FHIR_JSON_NEW);
|
||||||
myClient.create().resource(subscription).execute();
|
myClient.create().resource(subscription).execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1117,9 +1073,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
@Test
|
@Test
|
||||||
public void testInvalidProcedureRequestParam() {
|
public void testInvalidProcedureRequestParam() {
|
||||||
assertThrows(UnprocessableEntityException.class, () -> {
|
assertThrows(UnprocessableEntityException.class, () -> {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String criteriabad = "ProcedureRequest?intent=instance-order&category=Laboratory";
|
String criteriabad = "ProcedureRequest?intent=instance-order&category=Laboratory";
|
||||||
Subscription subscription = newSubscription(criteriabad, payload);
|
Subscription subscription = newSubscription(criteriabad, Constants.CT_FHIR_JSON_NEW);
|
||||||
myClient.create().resource(subscription).execute();
|
myClient.create().resource(subscription).execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1127,9 +1082,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
@Test
|
@Test
|
||||||
public void testInvalidBodySiteParam() {
|
public void testInvalidBodySiteParam() {
|
||||||
assertThrows(UnprocessableEntityException.class, () -> {
|
assertThrows(UnprocessableEntityException.class, () -> {
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String criteriabad = "BodySite?accessType=Catheter";
|
String criteriabad = "BodySite?accessType=Catheter";
|
||||||
Subscription subscription = newSubscription(criteriabad, payload);
|
Subscription subscription = newSubscription(criteriabad, Constants.CT_FHIR_JSON_NEW);
|
||||||
myClient.create().resource(subscription).execute();
|
myClient.create().resource(subscription).execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1137,9 +1091,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
@Test
|
@Test
|
||||||
public void testGoodSubscriptionPersists() {
|
public void testGoodSubscriptionPersists() {
|
||||||
assertEquals(0, subscriptionCount());
|
assertEquals(0, subscriptionCount());
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String criteriaGood = "Patient?gender=male";
|
String criteriaGood = "Patient?gender=male";
|
||||||
Subscription subscription = newSubscription(criteriaGood, payload);
|
Subscription subscription = newSubscription(criteriaGood, Constants.CT_FHIR_JSON_NEW);
|
||||||
myClient.create().resource(subscription).execute();
|
myClient.create().resource(subscription).execute();
|
||||||
await().until(() -> subscriptionCount() == 1);
|
await().until(() -> subscriptionCount() == 1);
|
||||||
}
|
}
|
||||||
|
@ -1188,9 +1141,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
@Test
|
@Test
|
||||||
public void testBadSubscriptionDoesntPersist() {
|
public void testBadSubscriptionDoesntPersist() {
|
||||||
assertEquals(0, subscriptionCount());
|
assertEquals(0, subscriptionCount());
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String criteriaBad = "BodySite?accessType=Catheter";
|
String criteriaBad = "BodySite?accessType=Catheter";
|
||||||
Subscription subscription = newSubscription(criteriaBad, payload);
|
Subscription subscription = newSubscription(criteriaBad, Constants.CT_FHIR_JSON_NEW);
|
||||||
try {
|
try {
|
||||||
myClient.create().resource(subscription).execute();
|
myClient.create().resource(subscription).execute();
|
||||||
} catch (UnprocessableEntityException e) {
|
} catch (UnprocessableEntityException e) {
|
||||||
|
|
|
@ -175,7 +175,7 @@ public class SubscriptionTopicR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
return mySubscriptionTopicRegistry.size() == theTarget;
|
return mySubscriptionTopicRegistry.size() == theTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SubscriptionTopic createEncounterSubscriptionTopic(Encounter.EncounterStatus theFrom, Encounter.EncounterStatus theCurrent, SubscriptionTopic.InteractionTrigger... theInteractionTriggers) {
|
private SubscriptionTopic createEncounterSubscriptionTopic(Encounter.EncounterStatus theFrom, Encounter.EncounterStatus theCurrent, SubscriptionTopic.InteractionTrigger... theInteractionTriggers) throws InterruptedException {
|
||||||
SubscriptionTopic retval = new SubscriptionTopic();
|
SubscriptionTopic retval = new SubscriptionTopic();
|
||||||
retval.setUrl(SUBSCRIPTION_TOPIC_TEST_URL);
|
retval.setUrl(SUBSCRIPTION_TOPIC_TEST_URL);
|
||||||
retval.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
retval.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||||
|
@ -188,7 +188,9 @@ public class SubscriptionTopicR4BTest extends BaseSubscriptionsR4BTest {
|
||||||
queryCriteria.setPrevious("Encounter?status=" + theFrom.toCode());
|
queryCriteria.setPrevious("Encounter?status=" + theFrom.toCode());
|
||||||
queryCriteria.setCurrent("Encounter?status=" + theCurrent.toCode());
|
queryCriteria.setCurrent("Encounter?status=" + theCurrent.toCode());
|
||||||
queryCriteria.setRequireBoth(true);
|
queryCriteria.setRequireBoth(true);
|
||||||
|
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
|
||||||
mySubscriptionTopicDao.create(retval, mySrd);
|
mySubscriptionTopicDao.create(retval, mySrd);
|
||||||
|
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||||
return retval;
|
return retval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,27 @@
|
||||||
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.interceptor.api.IInterceptorService;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
||||||
|
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||||
|
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||||
import ca.uhn.fhir.jpa.provider.r5.BaseResourceProviderR5Test;
|
import ca.uhn.fhir.jpa.provider.r5.BaseResourceProviderR5Test;
|
||||||
import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel;
|
import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
|
||||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor;
|
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor;
|
||||||
import ca.uhn.fhir.jpa.test.util.SubscriptionTestUtil;
|
import ca.uhn.fhir.jpa.test.util.SubscriptionTestUtil;
|
||||||
import ca.uhn.fhir.rest.annotation.Create;
|
import ca.uhn.fhir.jpa.topic.SubscriptionTopicLoader;
|
||||||
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
import ca.uhn.fhir.jpa.topic.SubscriptionTopicRegistry;
|
||||||
import ca.uhn.fhir.rest.annotation.Update;
|
import ca.uhn.fhir.rest.annotation.Transaction;
|
||||||
|
import ca.uhn.fhir.rest.annotation.TransactionParam;
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
|
||||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
import ca.uhn.fhir.test.utilities.JettyUtil;
|
import ca.uhn.fhir.test.utilities.JettyUtil;
|
||||||
import ca.uhn.fhir.util.BundleUtil;
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
import com.google.common.collect.Lists;
|
import ca.uhn.test.concurrency.PointcutLatch;
|
||||||
import net.ttddyy.dsproxy.QueryCount;
|
import net.ttddyy.dsproxy.QueryCount;
|
||||||
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
|
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
|
@ -25,12 +30,10 @@ import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
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 org.hl7.fhir.r5.model.Bundle;
|
import org.hl7.fhir.r5.model.Bundle;
|
||||||
import org.hl7.fhir.r5.model.CodeableConcept;
|
|
||||||
import org.hl7.fhir.r5.model.Coding;
|
|
||||||
import org.hl7.fhir.r5.model.Enumerations;
|
import org.hl7.fhir.r5.model.Enumerations;
|
||||||
import org.hl7.fhir.r5.model.IdType;
|
|
||||||
import org.hl7.fhir.r5.model.Observation;
|
import org.hl7.fhir.r5.model.Observation;
|
||||||
import org.hl7.fhir.r5.model.Subscription;
|
import org.hl7.fhir.r5.model.Subscription;
|
||||||
|
import org.hl7.fhir.r5.model.SubscriptionStatus;
|
||||||
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
@ -46,15 +49,20 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.awaitility.Awaitility.await;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
@Disabled("abstract")
|
@Disabled("abstract")
|
||||||
public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test {
|
public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test {
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSubscriptionsR5Test.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSubscriptionsR5Test.class);
|
||||||
|
public static final String SUBSCRIPTION_TOPIC_TEST_URL = "http://example.com/topic/test";
|
||||||
|
|
||||||
|
|
||||||
protected static int ourListenerPort;
|
protected static int ourListenerPort;
|
||||||
protected static List<String> ourContentTypes = Collections.synchronizedList(new ArrayList<>());
|
|
||||||
protected static List<String> ourHeaders = Collections.synchronizedList(new ArrayList<>());
|
|
||||||
protected static List<Observation> ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList());
|
|
||||||
protected static List<Observation> ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList());
|
|
||||||
private static Server ourListenerServer;
|
private static Server ourListenerServer;
|
||||||
private static SingleQueryCountHolder ourCountHolder;
|
private static SingleQueryCountHolder ourCountHolder;
|
||||||
private static String ourListenerServerBase;
|
private static String ourListenerServerBase;
|
||||||
|
@ -67,37 +75,28 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
|
||||||
protected List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>());
|
protected List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>());
|
||||||
@Autowired
|
@Autowired
|
||||||
private SingleQueryCountHolder myCountHolder;
|
private SingleQueryCountHolder myCountHolder;
|
||||||
|
@Autowired
|
||||||
|
protected SubscriptionTopicRegistry mySubscriptionTopicRegistry;
|
||||||
|
@Autowired
|
||||||
|
protected SubscriptionTopicLoader mySubscriptionTopicLoader;
|
||||||
|
@Autowired
|
||||||
|
private IInterceptorService myInterceptorService;
|
||||||
|
private static final SubscriptionTopicR5Test.TestSystemProvider ourTestSystemProvider = new SubscriptionTopicR5Test.TestSystemProvider();
|
||||||
|
protected IFhirResourceDao<SubscriptionTopic> mySubscriptionTopicDao;
|
||||||
|
protected final PointcutLatch mySubscriptionTopicsCheckedLatch = new PointcutLatch(Pointcut.SUBSCRIPTION_TOPIC_AFTER_PERSISTED_RESOURCE_CHECKED);
|
||||||
|
protected final PointcutLatch mySubscriptionDeliveredLatch = new PointcutLatch(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY);
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
public void afterUnregisterRestHookListener() {
|
|
||||||
for (IIdType next : mySubscriptionIds) {
|
|
||||||
IIdType nextId = next.toUnqualifiedVersionless();
|
|
||||||
ourLog.info("Deleting: {}", nextId);
|
|
||||||
myClient.delete().resourceById(nextId).execute();
|
|
||||||
}
|
|
||||||
mySubscriptionIds.clear();
|
|
||||||
|
|
||||||
myStorageSettings.setAllowMultipleDelete(true);
|
|
||||||
ourLog.info("Deleting all subscriptions");
|
|
||||||
myClient.delete().resourceConditionalByUrl("Subscription?status=active").execute();
|
|
||||||
myClient.delete().resourceConditionalByUrl("Observation?code:missing=false").execute();
|
|
||||||
ourLog.info("Done deleting all subscriptions");
|
|
||||||
myStorageSettings.setAllowMultipleDelete(new JpaStorageSettings().isAllowMultipleDelete());
|
|
||||||
|
|
||||||
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Override
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void beforeRegisterRestHookListener() {
|
protected void before() throws Exception {
|
||||||
|
super.before();
|
||||||
|
mySubscriptionTopicDao = myDaoRegistry.getResourceDao(SubscriptionTopic.class);
|
||||||
mySubscriptionTestUtil.registerRestHookInterceptor();
|
mySubscriptionTestUtil.registerRestHookInterceptor();
|
||||||
}
|
ourListenerRestServer.unregisterProvider(mySystemProvider);
|
||||||
|
ourListenerRestServer.registerProvider(ourTestSystemProvider);
|
||||||
|
|
||||||
@BeforeEach
|
ourTestSystemProvider.clear();
|
||||||
public void beforeReset() throws Exception {
|
|
||||||
ourCreatedObservations.clear();
|
|
||||||
ourUpdatedObservations.clear();
|
|
||||||
ourContentTypes.clear();
|
|
||||||
ourHeaders.clear();
|
|
||||||
|
|
||||||
// Delete all Subscriptions
|
// Delete all Subscriptions
|
||||||
if (myClient != null) {
|
if (myClient != null) {
|
||||||
|
@ -116,34 +115,77 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
|
||||||
if (processingChannel != null) {
|
if (processingChannel != null) {
|
||||||
processingChannel.addInterceptor(myCountingInterceptor);
|
processingChannel.addInterceptor(myCountingInterceptor);
|
||||||
}
|
}
|
||||||
|
myInterceptorService.registerAnonymousInterceptor(Pointcut.SUBSCRIPTION_TOPIC_AFTER_PERSISTED_RESOURCE_CHECKED, mySubscriptionTopicsCheckedLatch);
|
||||||
|
myInterceptorService.registerAnonymousInterceptor(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY, mySubscriptionDeliveredLatch);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void afterUnregisterRestHookListener() {
|
||||||
|
myInterceptorService.unregisterAllAnonymousInterceptors();
|
||||||
|
for (IIdType next : mySubscriptionIds) {
|
||||||
|
IIdType nextId = next.toUnqualifiedVersionless();
|
||||||
|
ourLog.info("Deleting: {}", nextId);
|
||||||
|
myClient.delete().resourceById(nextId).execute();
|
||||||
|
}
|
||||||
|
mySubscriptionIds.clear();
|
||||||
|
|
||||||
protected Subscription createSubscription(String theCriteria, String thePayload) {
|
myStorageSettings.setAllowMultipleDelete(true);
|
||||||
Subscription subscription = newSubscription(theCriteria, thePayload);
|
ourLog.info("Deleting all subscriptions");
|
||||||
|
myClient.delete().resourceConditionalByUrl("Subscription?status=active").execute();
|
||||||
|
myClient.delete().resourceConditionalByUrl("Observation?code:missing=false").execute();
|
||||||
|
ourLog.info("Done deleting all subscriptions");
|
||||||
|
myStorageSettings.setAllowMultipleDelete(new JpaStorageSettings().isAllowMultipleDelete());
|
||||||
|
|
||||||
return postSubscription(subscription);
|
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
|
||||||
|
ourListenerRestServer.unregisterProvider(ourTestSystemProvider);
|
||||||
|
ourListenerRestServer.registerProvider(mySystemProvider);
|
||||||
|
mySubscriptionTopicsCheckedLatch.clear();
|
||||||
|
mySubscriptionDeliveredLatch.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getSystemProviderCount() {
|
||||||
|
return ourTestSystemProvider.getCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> getLastSystemProviderHeaders() {
|
||||||
|
return ourTestSystemProvider.getLastHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Bundle getLastSystemProviderBundle() {
|
||||||
|
return ourTestSystemProvider.getLastBundle();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getLastSystemProviderContentType() {
|
||||||
|
return ourTestSystemProvider.getLastContentType();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Set<Observation> getReceivedObservations() {
|
||||||
|
return ourTestSystemProvider.receivedBundles.stream()
|
||||||
|
.flatMap(t -> t.getEntry().stream())
|
||||||
|
.filter(t -> t.getResource() instanceof Observation)
|
||||||
|
.map(t -> (Observation) t.getResource())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
protected Subscription postSubscription(Subscription subscription) {
|
protected Subscription postSubscription(Subscription subscription) throws InterruptedException {
|
||||||
|
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
|
||||||
MethodOutcome methodOutcome = myClient.create().resource(subscription).execute();
|
MethodOutcome methodOutcome = myClient.create().resource(subscription).execute();
|
||||||
|
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||||
|
|
||||||
subscription.setId(methodOutcome.getId().toVersionless());
|
subscription.setId(methodOutcome.getId().toVersionless());
|
||||||
mySubscriptionIds.add(methodOutcome.getId());
|
mySubscriptionIds.add(methodOutcome.getId());
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Subscription newSubscription(String theCriteria, String thePayload) {
|
protected Subscription newTopicSubscription(String theTopicUrl, String thePayload) {
|
||||||
SubscriptionTopic topic = new SubscriptionTopic();
|
|
||||||
topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(theCriteria);
|
|
||||||
topic.setId("1");
|
|
||||||
|
|
||||||
Subscription subscription = new Subscription();
|
Subscription subscription = new Subscription();
|
||||||
subscription.getContained().add(topic);
|
subscription.setTopic(theTopicUrl);
|
||||||
subscription.setTopic("#1");
|
|
||||||
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
|
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
|
||||||
subscription.setStatus(Enumerations.SubscriptionStatusCodes.REQUESTED);
|
subscription.setStatus(Enumerations.SubscriptionStatusCodes.ACTIVE);
|
||||||
|
|
||||||
subscription.getChannelType()
|
subscription.getChannelType()
|
||||||
.setSystem(CanonicalSubscriptionChannelType.RESTHOOK.getSystem())
|
.setSystem(CanonicalSubscriptionChannelType.RESTHOOK.getSystem())
|
||||||
|
@ -153,72 +195,56 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void waitForQueueToDrain() throws InterruptedException {
|
|
||||||
mySubscriptionTestUtil.waitForQueueToDrain();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void initializeOurCountHolder() {
|
public void initializeOurCountHolder() {
|
||||||
ourCountHolder = myCountHolder;
|
ourCountHolder = myCountHolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WIP STR5 consolidate with lambda
|
||||||
protected Observation sendObservation(String code, String system) {
|
protected IIdType createResource(IBaseResource theResource, boolean theExpectDelivery) throws InterruptedException {
|
||||||
Observation observation = new Observation();
|
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
|
||||||
CodeableConcept codeableConcept = new CodeableConcept();
|
if (theExpectDelivery) {
|
||||||
observation.setCode(codeableConcept);
|
mySubscriptionDeliveredLatch.setExpectedCount(1);
|
||||||
observation.getIdentifierFirstRep().setSystem("foo").setValue("1");
|
}
|
||||||
Coding coding = codeableConcept.addCoding();
|
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
|
||||||
coding.setCode(code);
|
IIdType id = dao.create(theResource, mySrd).getId();
|
||||||
coding.setSystem(system);
|
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||||
|
if (theExpectDelivery) {
|
||||||
observation.setStatus(Enumerations.ObservationStatus.FINAL);
|
mySubscriptionDeliveredLatch.awaitExpected();
|
||||||
|
}
|
||||||
IIdType id = myObservationDao.create(observation).getId();
|
return id;
|
||||||
observation.setId(id);
|
|
||||||
|
|
||||||
return observation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected DaoMethodOutcome updateResource(IBaseResource theResource, boolean theExpectDelivery) throws InterruptedException {
|
||||||
|
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
|
||||||
|
if (theExpectDelivery) {
|
||||||
|
mySubscriptionDeliveredLatch.setExpectedCount(1);
|
||||||
|
}
|
||||||
|
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
|
||||||
|
DaoMethodOutcome retval = dao.update(theResource, mySrd);
|
||||||
|
|
||||||
public static class ObservationListener implements IResourceProvider {
|
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||||
|
ResourceModifiedMessage lastMessage = mySubscriptionTopicsCheckedLatch.getLatchInvocationParameterOfType(ResourceModifiedMessage.class);
|
||||||
|
assertEquals(theResource.getIdElement().toVersionless().toString(), lastMessage.getPayloadId());
|
||||||
|
|
||||||
@Create
|
if (theExpectDelivery) {
|
||||||
public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
|
mySubscriptionDeliveredLatch.awaitExpected();
|
||||||
ourLog.info("Received Listener Create");
|
}
|
||||||
ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
|
return retval;
|
||||||
ourCreatedObservations.add(theObservation);
|
|
||||||
extractHeaders(theRequest);
|
|
||||||
return new MethodOutcome(new IdType("Observation/1"), true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void extractHeaders(HttpServletRequest theRequest) {
|
protected Bundle sendTransaction(Bundle theBundle, boolean theExpectDelivery) throws InterruptedException {
|
||||||
Enumeration<String> headerNamesEnum = theRequest.getHeaderNames();
|
int expectedChecks = theBundle.getEntry().size();
|
||||||
while (headerNamesEnum.hasMoreElements()) {
|
if (theExpectDelivery) {
|
||||||
String nextName = headerNamesEnum.nextElement();
|
mySubscriptionDeliveredLatch.setExpectedCount(1);
|
||||||
Enumeration<String> valueEnum = theRequest.getHeaders(nextName);
|
|
||||||
while (valueEnum.hasMoreElements()) {
|
|
||||||
String nextValue = valueEnum.nextElement();
|
|
||||||
ourHeaders.add(nextName + ": " + nextValue);
|
|
||||||
}
|
}
|
||||||
|
mySubscriptionTopicsCheckedLatch.setExpectedCount(expectedChecks);
|
||||||
|
Bundle retval = mySystemDao.transaction(mySrd, theBundle);
|
||||||
|
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||||
|
if (theExpectDelivery) {
|
||||||
|
mySubscriptionDeliveredLatch.awaitExpected();
|
||||||
}
|
}
|
||||||
}
|
return retval;
|
||||||
|
|
||||||
@Override
|
|
||||||
public Class<? extends IBaseResource> getResourceType() {
|
|
||||||
return Observation.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Update
|
|
||||||
public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
|
|
||||||
ourLog.info("Received Listener Update");
|
|
||||||
ourUpdatedObservations.add(theObservation);
|
|
||||||
ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
|
|
||||||
extractHeaders(theRequest);
|
|
||||||
return new MethodOutcome(new IdType("Observation/1"), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
|
@ -230,13 +256,45 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
|
||||||
return ourCountHolder.getQueryCountMap().get("");
|
return ourCountHolder.getQueryCountMap().get("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void waitForRegisteredSubscriptionTopicCount(int theTarget) {
|
||||||
|
await().until(() -> subscriptionTopicRegistryHasSize(theTarget));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean subscriptionTopicRegistryHasSize(int theTarget) {
|
||||||
|
int size = mySubscriptionTopicRegistry.size();
|
||||||
|
if (size == theTarget) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
mySubscriptionTopicLoader.doSyncResourcessForUnitTest();
|
||||||
|
return mySubscriptionTopicRegistry.size() == theTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SubscriptionTopic createSubscriptionTopic(SubscriptionTopic theSubscriptionTopic) throws InterruptedException {
|
||||||
|
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
|
||||||
|
SubscriptionTopic retval = (SubscriptionTopic) myClient.create().resource(theSubscriptionTopic).execute().getResource();
|
||||||
|
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void validateSubscriptionStatus(Subscription subscription, IBaseResource sentResource, SubscriptionStatus ss) {
|
||||||
|
assertEquals(Enumerations.SubscriptionStatusCodes.ACTIVE, ss.getStatus());
|
||||||
|
assertEquals(SubscriptionStatus.SubscriptionNotificationType.EVENTNOTIFICATION, ss.getType());
|
||||||
|
assertEquals("1", ss.getEventsSinceSubscriptionStartElement().getValueAsString());
|
||||||
|
|
||||||
|
List<SubscriptionStatus.SubscriptionStatusNotificationEventComponent> notificationEvents = ss.getNotificationEvent();
|
||||||
|
assertEquals(1, notificationEvents.size());
|
||||||
|
SubscriptionStatus.SubscriptionStatusNotificationEventComponent notificationEvent = notificationEvents.get(0);
|
||||||
|
assertEquals(1, notificationEvent.getEventNumber());
|
||||||
|
assertEquals(sentResource.getIdElement().toUnqualifiedVersionless(), notificationEvent.getFocus().getReferenceElement());
|
||||||
|
|
||||||
|
assertEquals(subscription.getIdElement().toUnqualifiedVersionless(), ss.getSubscription().getReferenceElement());
|
||||||
|
assertEquals(subscription.getTopic(), ss.getTopic());
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
public static void startListenerServer() throws Exception {
|
public static void startListenerServer() throws Exception {
|
||||||
ourListenerRestServer = new RestfulServer(FhirContext.forR5Cached());
|
ourListenerRestServer = new RestfulServer(FhirContext.forR5Cached());
|
||||||
|
|
||||||
ObservationListener obsListener = new ObservationListener();
|
|
||||||
ourListenerRestServer.setResourceProviders(obsListener);
|
|
||||||
|
|
||||||
ourListenerServer = new Server(0);
|
ourListenerServer = new Server(0);
|
||||||
|
|
||||||
ServletContextHandler proxyHandler = new ServletContextHandler();
|
ServletContextHandler proxyHandler = new ServletContextHandler();
|
||||||
|
@ -257,4 +315,56 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
|
||||||
JettyUtil.closeServer(ourListenerServer);
|
JettyUtil.closeServer(ourListenerServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class TestSystemProvider {
|
||||||
|
AtomicInteger count = new AtomicInteger(0);
|
||||||
|
final List<Bundle> receivedBundles = new ArrayList<>();
|
||||||
|
final List<String> receivedContentTypes = new ArrayList<>();
|
||||||
|
final List<String> myHeaders = new ArrayList<>();
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public Bundle transaction(@TransactionParam Bundle theBundle, HttpServletRequest theRequest) {
|
||||||
|
ourLog.info("Received Transaction with {} entries", theBundle.getEntry().size());
|
||||||
|
count.incrementAndGet();
|
||||||
|
receivedContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
|
||||||
|
receivedBundles.add(theBundle);
|
||||||
|
extractHeaders(theRequest);
|
||||||
|
return theBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extractHeaders(HttpServletRequest theRequest) {
|
||||||
|
Enumeration<String> headerNamesEnum = theRequest.getHeaderNames();
|
||||||
|
while (headerNamesEnum.hasMoreElements()) {
|
||||||
|
String nextName = headerNamesEnum.nextElement();
|
||||||
|
Enumeration<String> valueEnum = theRequest.getHeaders(nextName);
|
||||||
|
while (valueEnum.hasMoreElements()) {
|
||||||
|
String nextValue = valueEnum.nextElement();
|
||||||
|
myHeaders.add(nextName + ": " + nextValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int getCount() {
|
||||||
|
return count.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastContentType() {
|
||||||
|
return receivedContentTypes.get(receivedContentTypes.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bundle getLastBundle() {
|
||||||
|
return receivedBundles.get(receivedBundles.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getLastHeaders() {
|
||||||
|
return Collections.unmodifiableList(myHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
count.set(0);
|
||||||
|
receivedBundles.clear();
|
||||||
|
receivedContentTypes.clear();
|
||||||
|
myHeaders.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
package ca.uhn.fhir.jpa.subscription;
|
package ca.uhn.fhir.jpa.subscription;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
|
||||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionConstants;
|
|
||||||
import ca.uhn.fhir.jpa.topic.SubscriptionTopicLoader;
|
|
||||||
import ca.uhn.fhir.jpa.topic.SubscriptionTopicRegistry;
|
|
||||||
import ca.uhn.fhir.rest.annotation.Transaction;
|
|
||||||
import ca.uhn.fhir.rest.annotation.TransactionParam;
|
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
import ca.uhn.fhir.util.BundleUtil;
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
@ -16,15 +10,11 @@ import org.hl7.fhir.r5.model.Enumerations;
|
||||||
import org.hl7.fhir.r5.model.Subscription;
|
import org.hl7.fhir.r5.model.Subscription;
|
||||||
import org.hl7.fhir.r5.model.SubscriptionStatus;
|
import org.hl7.fhir.r5.model.SubscriptionStatus;
|
||||||
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
import static org.awaitility.Awaitility.await;
|
import static org.awaitility.Awaitility.await;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
@ -32,31 +22,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test {
|
public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicR5Test.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicR5Test.class);
|
||||||
public static final String SUBSCRIPTION_TOPIC_TEST_URL = "http://example.com/topic/test";
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
protected SubscriptionTopicRegistry mySubscriptionTopicRegistry;
|
|
||||||
@Autowired
|
|
||||||
protected SubscriptionTopicLoader mySubscriptionTopicLoader;
|
|
||||||
protected IFhirResourceDao<SubscriptionTopic> mySubscriptionTopicDao;
|
|
||||||
private static final TestSystemProvider ourTestSystemProvider = new TestSystemProvider();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@BeforeEach
|
|
||||||
protected void before() throws Exception {
|
|
||||||
super.before();
|
|
||||||
ourListenerRestServer.unregisterProvider(mySystemProvider);
|
|
||||||
ourListenerRestServer.registerProvider(ourTestSystemProvider);
|
|
||||||
mySubscriptionTopicDao = myDaoRegistry.getResourceDao(SubscriptionTopic.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@AfterEach
|
|
||||||
public void after() throws Exception {
|
|
||||||
ourListenerRestServer.unregisterProvider(ourTestSystemProvider);
|
|
||||||
ourListenerRestServer.registerProvider(mySystemProvider);
|
|
||||||
super.after();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSubscriptionTopicRegistryBean() {
|
public void testSubscriptionTopicRegistryBean() {
|
||||||
|
@ -72,15 +37,12 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test {
|
||||||
Subscription subscription = createTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL);
|
Subscription subscription = createTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL);
|
||||||
waitForActivatedSubscriptionCount(1);
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
assertEquals(0, ourTestSystemProvider.getCount());
|
assertEquals(0, getSystemProviderCount());
|
||||||
Encounter sentEncounter = sendEncounterWithStatus(Encounter.EncounterStatus.COMPLETED);
|
Encounter sentEncounter = sendEncounterWithStatus(Encounter.EncounterStatus.COMPLETED, false);
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
await().until(() -> getSystemProviderCount() > 0);
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
await().until(() -> ourTestSystemProvider.getCount() > 0);
|
Bundle receivedBundle = getLastSystemProviderBundle();
|
||||||
|
|
||||||
Bundle receivedBundle = ourTestSystemProvider.getLastInput();
|
|
||||||
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, receivedBundle);
|
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, receivedBundle);
|
||||||
assertEquals(2, resources.size());
|
assertEquals(2, resources.size());
|
||||||
|
|
||||||
|
@ -92,41 +54,14 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test {
|
||||||
assertEquals(sentEncounter.getIdElement(), encounter.getIdElement());
|
assertEquals(sentEncounter.getIdElement(), encounter.getIdElement());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateSubscriptionStatus(Subscription subscription, Encounter sentEncounter, SubscriptionStatus ss) {
|
|
||||||
assertEquals(Enumerations.SubscriptionStatusCodes.ACTIVE, ss.getStatus());
|
|
||||||
assertEquals(SubscriptionStatus.SubscriptionNotificationType.EVENTNOTIFICATION, ss.getType());
|
|
||||||
assertEquals("1", ss.getEventsSinceSubscriptionStartElement().getValueAsString());
|
|
||||||
|
|
||||||
List<SubscriptionStatus.SubscriptionStatusNotificationEventComponent> notificationEvents = ss.getNotificationEvent();
|
|
||||||
assertEquals(1, notificationEvents.size());
|
|
||||||
SubscriptionStatus.SubscriptionStatusNotificationEventComponent notificationEvent = notificationEvents.get(0);
|
|
||||||
assertEquals(1, notificationEvent.getEventNumber());
|
|
||||||
assertEquals(sentEncounter.getIdElement().toUnqualifiedVersionless(), notificationEvent.getFocus().getReferenceElement());
|
|
||||||
|
|
||||||
assertEquals(subscription.getIdElement().toUnqualifiedVersionless(), ss.getSubscription().getReferenceElement());
|
private Subscription createTopicSubscription(String theTopicUrl) throws InterruptedException {
|
||||||
assertEquals(SUBSCRIPTION_TOPIC_TEST_URL, ss.getTopic());
|
Subscription subscription = newTopicSubscription(theTopicUrl, Constants.CT_FHIR_JSON_NEW);
|
||||||
}
|
|
||||||
|
|
||||||
private Subscription createTopicSubscription(String theTopicUrl) {
|
|
||||||
Subscription subscription = newSubscription(theTopicUrl, Constants.CT_FHIR_JSON_NEW);
|
|
||||||
subscription.getMeta().addProfile(SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL);
|
|
||||||
return postSubscription(subscription);
|
return postSubscription(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void waitForRegisteredSubscriptionTopicCount(int theTarget) throws Exception {
|
private SubscriptionTopic createEncounterSubscriptionTopic(Encounter.EncounterStatus theFrom, Encounter.EncounterStatus theCurrent, SubscriptionTopic.InteractionTrigger... theInteractionTriggers) throws InterruptedException {
|
||||||
await().until(() -> subscriptionTopicRegistryHasSize(theTarget));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean subscriptionTopicRegistryHasSize(int theTarget) {
|
|
||||||
int size = mySubscriptionTopicRegistry.size();
|
|
||||||
if (size == theTarget) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
mySubscriptionTopicLoader.doSyncResourcessForUnitTest();
|
|
||||||
return mySubscriptionTopicRegistry.size() == theTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SubscriptionTopic createEncounterSubscriptionTopic(Encounter.EncounterStatus theFrom, Encounter.EncounterStatus theCurrent, SubscriptionTopic.InteractionTrigger... theInteractionTriggers) {
|
|
||||||
SubscriptionTopic retval = new SubscriptionTopic();
|
SubscriptionTopic retval = new SubscriptionTopic();
|
||||||
retval.setUrl(SUBSCRIPTION_TOPIC_TEST_URL);
|
retval.setUrl(SUBSCRIPTION_TOPIC_TEST_URL);
|
||||||
retval.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
retval.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||||
|
@ -139,37 +74,18 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test {
|
||||||
queryCriteria.setPrevious("Encounter?status=" + theFrom.toCode());
|
queryCriteria.setPrevious("Encounter?status=" + theFrom.toCode());
|
||||||
queryCriteria.setCurrent("Encounter?status=" + theCurrent.toCode());
|
queryCriteria.setCurrent("Encounter?status=" + theCurrent.toCode());
|
||||||
queryCriteria.setRequireBoth(true);
|
queryCriteria.setRequireBoth(true);
|
||||||
queryCriteria.setRequireBoth(true);
|
super.createResource(retval, false);
|
||||||
mySubscriptionTopicDao.create(retval, mySrd);
|
|
||||||
return retval;
|
return retval;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Encounter sendEncounterWithStatus(Encounter.EncounterStatus theStatus) {
|
private Encounter sendEncounterWithStatus(Encounter.EncounterStatus theStatus, boolean theExpectDelivery) throws InterruptedException {
|
||||||
Encounter encounter = new Encounter();
|
Encounter encounter = new Encounter();
|
||||||
encounter.setStatus(theStatus);
|
encounter.setStatus(theStatus);
|
||||||
|
|
||||||
IIdType id = myEncounterDao.create(encounter, mySrd).getId();
|
IIdType id = createResource(encounter, theExpectDelivery);
|
||||||
encounter.setId(id);
|
encounter.setId(id);
|
||||||
return encounter;
|
return encounter;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class TestSystemProvider {
|
|
||||||
AtomicInteger myCount = new AtomicInteger(0);
|
|
||||||
Bundle myLastInput;
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
public Bundle transaction(@TransactionParam Bundle theInput) {
|
|
||||||
myCount.incrementAndGet();
|
|
||||||
myLastInput = theInput;
|
|
||||||
return theInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCount() {
|
|
||||||
return myCount.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bundle getLastInput() {
|
|
||||||
return myLastInput;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,884 @@
|
||||||
|
package ca.uhn.fhir.jpa.subscription.resthook;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
|
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR5Test;
|
||||||
|
import ca.uhn.fhir.jpa.test.util.StoppableSubscriptionDeliveringRestHookSubscriber;
|
||||||
|
import ca.uhn.fhir.rest.api.CacheControlDirective;
|
||||||
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
|
import ca.uhn.fhir.util.HapiExtensions;
|
||||||
|
import org.hamcrest.MatcherAssert;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
import org.hl7.fhir.r5.model.BooleanType;
|
||||||
|
import org.hl7.fhir.r5.model.Bundle;
|
||||||
|
import org.hl7.fhir.r5.model.CodeableConcept;
|
||||||
|
import org.hl7.fhir.r5.model.Coding;
|
||||||
|
import org.hl7.fhir.r5.model.Enumerations;
|
||||||
|
import org.hl7.fhir.r5.model.IdType;
|
||||||
|
import org.hl7.fhir.r5.model.Observation;
|
||||||
|
import org.hl7.fhir.r5.model.Patient;
|
||||||
|
import org.hl7.fhir.r5.model.SearchParameter;
|
||||||
|
import org.hl7.fhir.r5.model.Subscription;
|
||||||
|
import org.hl7.fhir.r5.model.SubscriptionStatus;
|
||||||
|
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.hamcrest.Matchers.matchesPattern;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the rest-hook subscriptions
|
||||||
|
*/
|
||||||
|
public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(RestHookTestR5IT.class);
|
||||||
|
public static final String OBS_CODE = "1000000050";
|
||||||
|
public static final String OBS_CODE2 = OBS_CODE + "111";
|
||||||
|
private static final String CUSTOM_URL = "http://custom.topic.url";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
StoppableSubscriptionDeliveringRestHookSubscriber myStoppableSubscriptionDeliveringRestHookSubscriber;
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void cleanupStoppableSubscriptionDeliveringRestHookSubscriber() {
|
||||||
|
ourLog.info("@AfterEach");
|
||||||
|
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(null);
|
||||||
|
myStoppableSubscriptionDeliveringRestHookSubscriber.unPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionApplicationFhirJson() throws Exception {
|
||||||
|
createObservationSubscriptionTopic(OBS_CODE);
|
||||||
|
createObservationSubscriptionTopic(OBS_CODE2);
|
||||||
|
waitForRegisteredSubscriptionTopicCount(2);
|
||||||
|
|
||||||
|
// WIP STR5 will likely require matching TopicSubscription
|
||||||
|
Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW);
|
||||||
|
|
||||||
|
Subscription subscription = postSubscription(subscription1);
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
Observation sentObservation = sendObservationExpectDelivery();
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
|
||||||
|
Observation obs = assertBundleAndGetObservation(subscription, sentObservation);
|
||||||
|
assertEquals(Enumerations.ObservationStatus.FINAL, obs.getStatus());
|
||||||
|
assertEquals(sentObservation.getIdElement(), obs.getIdElement());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Observation sendObservationExpectDelivery() throws InterruptedException {
|
||||||
|
return sendObservation(OBS_CODE, "SNOMED-CT", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdatesHaveCorrectMetadata() throws Exception {
|
||||||
|
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
Subscription subscription = createMatchingTopicSubscription();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Send version 1
|
||||||
|
*/
|
||||||
|
|
||||||
|
Observation sentObservation = sendObservationExpectDelivery();
|
||||||
|
sentObservation = myObservationDao.read(sentObservation.getIdElement().toUnqualifiedVersionless());
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
|
||||||
|
Observation receivedObs = assertBundleAndGetObservation(subscription, sentObservation);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType());
|
||||||
|
Assertions.assertEquals("1", receivedObs.getIdElement().getVersionIdPart());
|
||||||
|
Assertions.assertEquals("1", receivedObs.getMeta().getVersionId());
|
||||||
|
Assertions.assertEquals(sentObservation.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString());
|
||||||
|
Assertions.assertEquals(sentObservation.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString());
|
||||||
|
Assertions.assertEquals("1", receivedObs.getIdentifierFirstRep().getValue());
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Send version 2
|
||||||
|
*/
|
||||||
|
|
||||||
|
sentObservation.getIdentifierFirstRep().setSystem("foo").setValue("2");
|
||||||
|
updateResource(sentObservation, true);
|
||||||
|
sentObservation = myObservationDao.read(sentObservation.getIdElement().toUnqualifiedVersionless());
|
||||||
|
|
||||||
|
// Should see a second subscription notification
|
||||||
|
assertReceivedTransactionCount(2);
|
||||||
|
|
||||||
|
receivedObs = assertBundleAndGetObservation(subscription, sentObservation);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType());
|
||||||
|
Assertions.assertEquals("2", receivedObs.getIdElement().getVersionIdPart());
|
||||||
|
Assertions.assertEquals("2", receivedObs.getMeta().getVersionId());
|
||||||
|
Assertions.assertEquals(sentObservation.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString());
|
||||||
|
Assertions.assertEquals(sentObservation.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString());
|
||||||
|
Assertions.assertEquals("2", receivedObs.getIdentifierFirstRep().getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Subscription createMatchingTopicSubscription() throws Exception {
|
||||||
|
Subscription subscription = createTopicSubscription(OBS_CODE);
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlaceholderReferencesInTransactionAreResolvedCorrectly() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
Subscription subscription = createMatchingTopicSubscription();
|
||||||
|
|
||||||
|
// 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 sentObservation = new Observation();
|
||||||
|
sentObservation.getIdentifierFirstRep().setSystem("foo").setValue("1");
|
||||||
|
sentObservation.getCode().addCoding().setCode(OBS_CODE).setSystem("SNOMED-CT");
|
||||||
|
sentObservation.setStatus(Enumerations.ObservationStatus.FINAL);
|
||||||
|
sentObservation.getSubject().setReference(patient.getId());
|
||||||
|
bundle.addEntry().setResource(sentObservation).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Observation");
|
||||||
|
|
||||||
|
// Send the transaction
|
||||||
|
sendTransaction(bundle, true);
|
||||||
|
|
||||||
|
Observation receivedObs = assertBundleAndGetObservation(subscription, sentObservation);
|
||||||
|
|
||||||
|
MatcherAssert.assertThat(receivedObs.getSubject().getReference(), matchesPattern("Patient/[0-9]+"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
Subscription subscription = createMatchingTopicSubscription();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Send version 1
|
||||||
|
*/
|
||||||
|
|
||||||
|
Observation sentObservation = new Observation();
|
||||||
|
sentObservation.getIdentifierFirstRep().setSystem("foo").setValue("1");
|
||||||
|
sentObservation.getCode().addCoding().setCode(OBS_CODE).setSystem("SNOMED-CT");
|
||||||
|
sentObservation.setStatus(Enumerations.ObservationStatus.FINAL);
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(Bundle.BundleType.TRANSACTION);
|
||||||
|
bundle.addEntry().setResource(sentObservation).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Observation");
|
||||||
|
// Send the transaction
|
||||||
|
Bundle responseBundle = sendTransaction(bundle, true);
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
|
||||||
|
Observation receivedObs = assertBundleAndGetObservation(subscription, sentObservation);
|
||||||
|
|
||||||
|
Observation obs = myObservationDao.read(new IdType(responseBundle.getEntry().get(0).getResponse().getLocation()));
|
||||||
|
|
||||||
|
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType());
|
||||||
|
Assertions.assertEquals("1", receivedObs.getIdElement().getVersionIdPart());
|
||||||
|
Assertions.assertEquals("1", receivedObs.getMeta().getVersionId());
|
||||||
|
Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString());
|
||||||
|
Assertions.assertEquals("1", receivedObs.getIdentifierFirstRep().getValue());
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Send version 2
|
||||||
|
*/
|
||||||
|
|
||||||
|
sentObservation = new Observation();
|
||||||
|
sentObservation.setId(obs.getId());
|
||||||
|
sentObservation.getIdentifierFirstRep().setSystem("foo").setValue("2");
|
||||||
|
sentObservation.getCode().addCoding().setCode(OBS_CODE).setSystem("SNOMED-CT");
|
||||||
|
sentObservation.setStatus(Enumerations.ObservationStatus.FINAL);
|
||||||
|
bundle = new Bundle();
|
||||||
|
bundle.setType(Bundle.BundleType.TRANSACTION);
|
||||||
|
bundle.addEntry().setResource(sentObservation).getRequest().setMethod(Bundle.HTTPVerb.PUT).setUrl(obs.getIdElement().toUnqualifiedVersionless().getValue());
|
||||||
|
// Send the transaction
|
||||||
|
sendTransaction(bundle, true);
|
||||||
|
assertReceivedTransactionCount(2);
|
||||||
|
|
||||||
|
receivedObs = assertBundleAndGetObservation(subscription, sentObservation);
|
||||||
|
obs = myObservationDao.read(obs.getIdElement().toUnqualifiedVersionless());
|
||||||
|
|
||||||
|
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType());
|
||||||
|
Assertions.assertEquals("2", receivedObs.getIdElement().getVersionIdPart());
|
||||||
|
Assertions.assertEquals("2", receivedObs.getMeta().getVersionId());
|
||||||
|
Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString());
|
||||||
|
Assertions.assertEquals("2", receivedObs.getIdentifierFirstRep().getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRepeatedDeliveries() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
createTopicSubscription(OBS_CODE);
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
mySubscriptionTopicsCheckedLatch.setExpectedCount(100);
|
||||||
|
mySubscriptionDeliveredLatch.setExpectedCount(100);
|
||||||
|
// WIP STR5 I don't know the answer to this, but should we be bunching these up into a single delivery?
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
Observation observation = new Observation();
|
||||||
|
observation.getIdentifierFirstRep().setSystem("foo").setValue("ID" + i);
|
||||||
|
observation.getCode().addCoding().setCode(OBS_CODE).setSystem("SNOMED-CT");
|
||||||
|
observation.setStatus(Enumerations.ObservationStatus.FINAL);
|
||||||
|
myObservationDao.create(observation, mySrd);
|
||||||
|
}
|
||||||
|
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||||
|
mySubscriptionDeliveredLatch.awaitExpected();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testActiveSubscriptionShouldntReActivate() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
createTopicSubscription();
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
int changes = this.mySubscriptionLoader.doSyncSubscriptionsForUnitTest();
|
||||||
|
assertEquals(0, changes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Subscription createTopicSubscription() throws InterruptedException {
|
||||||
|
// WIP STR5 will likely require matching TopicSubscription
|
||||||
|
Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_JSON_NEW);
|
||||||
|
|
||||||
|
return postSubscription(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createSubscriptionTopic() throws InterruptedException {
|
||||||
|
createObservationSubscriptionTopic(OBS_CODE);
|
||||||
|
waitForRegisteredSubscriptionTopicCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionNoopUpdateDoesntTriggerNewDelivery() throws Exception {
|
||||||
|
createObservationSubscriptionTopic(OBS_CODE);
|
||||||
|
createObservationSubscriptionTopic(OBS_CODE2);
|
||||||
|
waitForRegisteredSubscriptionTopicCount(2);
|
||||||
|
|
||||||
|
Subscription subscription = createTopicSubscription();
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
Observation sentObservation = sendObservationExpectDelivery();
|
||||||
|
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
|
||||||
|
Observation obs = assertBundleAndGetObservation(subscription, sentObservation);
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType());
|
||||||
|
|
||||||
|
// Send an update with no changes
|
||||||
|
obs.setId(obs.getIdElement().toUnqualifiedVersionless());
|
||||||
|
myObservationDao.update(obs, mySrd);
|
||||||
|
|
||||||
|
// TODO KHS replace this sleep with a latch on http request processed
|
||||||
|
Thread.sleep(1000);
|
||||||
|
|
||||||
|
// Should be no further deliveries
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionApplicationJsonDisableVersionIdInDelivery() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
waitForActivatedSubscriptionCount(0);
|
||||||
|
Subscription subscription = createTopicSubscription();
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
ourLog.info("** About to send observation");
|
||||||
|
Observation sentObservation1 = sendObservationExpectDelivery();
|
||||||
|
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
|
||||||
|
Observation obs = assertBundleAndGetObservation(subscription, sentObservation1);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType());
|
||||||
|
|
||||||
|
IdType idElement = obs.getIdElement();
|
||||||
|
assertEquals(sentObservation1.getIdElement().getIdPart(), idElement.getIdPart());
|
||||||
|
// VersionId is present
|
||||||
|
assertEquals(sentObservation1.getIdElement().getVersionIdPart(), idElement.getVersionIdPart());
|
||||||
|
|
||||||
|
subscription
|
||||||
|
.addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS, new BooleanType("true"));
|
||||||
|
ourLog.info("** About to update subscription");
|
||||||
|
|
||||||
|
ourLog.info("** About to send another...");
|
||||||
|
updateResource(subscription, false);
|
||||||
|
|
||||||
|
ourLog.info("** About to send observation");
|
||||||
|
Observation sentObservation2 = sendObservationExpectDelivery();
|
||||||
|
|
||||||
|
assertReceivedTransactionCount(2);
|
||||||
|
|
||||||
|
Observation obs2 = assertBundleAndGetObservation(subscription, sentObservation2);
|
||||||
|
|
||||||
|
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType());
|
||||||
|
|
||||||
|
idElement =obs2.getIdElement();
|
||||||
|
assertEquals(sentObservation2.getIdElement().getIdPart(), idElement.getIdPart());
|
||||||
|
// Now VersionId is stripped
|
||||||
|
assertEquals(null, idElement.getVersionIdPart());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionDoesntGetLatestVersionByDefault() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
Subscription subscription = createTopicSubscription();
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
myStoppableSubscriptionDeliveringRestHookSubscriber.pause();
|
||||||
|
final CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||||
|
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch);
|
||||||
|
|
||||||
|
ourLog.info("** About to send observation");
|
||||||
|
Observation sentObservation = sendObservation(OBS_CODE, "SNOMED-CT", false);
|
||||||
|
assertEquals("1", sentObservation.getIdElement().getVersionIdPart());
|
||||||
|
assertNull(sentObservation.getNoteFirstRep().getText());
|
||||||
|
|
||||||
|
sentObservation.getNoteFirstRep().setText("changed");
|
||||||
|
|
||||||
|
DaoMethodOutcome methodOutcome = updateResource(sentObservation, false);
|
||||||
|
assertEquals("2", methodOutcome.getId().getVersionIdPart());
|
||||||
|
assertEquals("changed", sentObservation.getNoteFirstRep().getText());
|
||||||
|
|
||||||
|
// Wait for our two delivery channel threads to be paused
|
||||||
|
assertTrue(countDownLatch.await(5L, TimeUnit.SECONDS));
|
||||||
|
// Open the floodgates!
|
||||||
|
mySubscriptionDeliveredLatch.setExpectedCount(2);
|
||||||
|
myStoppableSubscriptionDeliveringRestHookSubscriber.unPause();
|
||||||
|
mySubscriptionDeliveredLatch.awaitExpected();
|
||||||
|
|
||||||
|
assertReceivedTransactionCount(2);
|
||||||
|
|
||||||
|
Observation observation1 = getReceivedObservations().stream()
|
||||||
|
.filter(t -> "1".equals(t.getIdElement().getVersionIdPart()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
Observation observation2 = getReceivedObservations().stream()
|
||||||
|
.filter(t -> "2".equals(t.getIdElement().getVersionIdPart()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
|
||||||
|
assertEquals("1", observation1.getIdElement().getVersionIdPart());
|
||||||
|
assertNull(observation1.getNoteFirstRep().getText());
|
||||||
|
assertEquals("2", observation2.getIdElement().getVersionIdPart());
|
||||||
|
assertEquals("changed", observation2.getNoteFirstRep().getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionGetsLatestVersionWithFlag() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_JSON_NEW);
|
||||||
|
subscription
|
||||||
|
.addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true"));
|
||||||
|
postSubscription(subscription);
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
myStoppableSubscriptionDeliveringRestHookSubscriber.pause();
|
||||||
|
final CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||||
|
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch);
|
||||||
|
|
||||||
|
ourLog.info("** About to send observation");
|
||||||
|
Observation sentObservation = sendObservation(OBS_CODE, "SNOMED-CT", false);
|
||||||
|
assertEquals("1", sentObservation.getIdElement().getVersionIdPart());
|
||||||
|
assertNull(sentObservation.getNoteFirstRep().getText());
|
||||||
|
|
||||||
|
sentObservation.getNoteFirstRep().setText("changed");
|
||||||
|
DaoMethodOutcome methodOutcome = updateResource(sentObservation, false);
|
||||||
|
assertEquals("2", methodOutcome.getId().getVersionIdPart());
|
||||||
|
assertEquals("changed", sentObservation.getNoteFirstRep().getText());
|
||||||
|
|
||||||
|
// Wait for our two delivery channel threads to be paused
|
||||||
|
assertTrue(countDownLatch.await(5L, TimeUnit.SECONDS));
|
||||||
|
// Open the floodgates!
|
||||||
|
mySubscriptionDeliveredLatch.setExpectedCount(2);
|
||||||
|
myStoppableSubscriptionDeliveringRestHookSubscriber.unPause();
|
||||||
|
mySubscriptionDeliveredLatch.awaitExpected();
|
||||||
|
|
||||||
|
assertTrue(getReceivedObservations().stream().allMatch(t -> "2".equals(t.getIdElement().getVersionIdPart())));
|
||||||
|
assertTrue(getReceivedObservations().stream().anyMatch(t -> "changed".equals(t.getNoteFirstRep().getText())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionApplicationJson() throws Exception {
|
||||||
|
createObservationSubscriptionTopic(OBS_CODE);
|
||||||
|
createObservationSubscriptionTopic(OBS_CODE2);
|
||||||
|
waitForRegisteredSubscriptionTopicCount(2);
|
||||||
|
|
||||||
|
Subscription subscription1 = createTopicSubscription();
|
||||||
|
// WIP STR5 will likely require matching TopicSubscription
|
||||||
|
Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_JSON_NEW);
|
||||||
|
|
||||||
|
Subscription subscription2 = postSubscription(subscription);
|
||||||
|
waitForActivatedSubscriptionCount(2);
|
||||||
|
|
||||||
|
Observation sentObservation1 = sendObservationExpectDelivery();
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
Observation receivedObs = assertBundleAndGetObservation(subscription1, sentObservation1);
|
||||||
|
assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType());
|
||||||
|
|
||||||
|
Assertions.assertEquals("1", receivedObs.getIdElement().getVersionIdPart());
|
||||||
|
|
||||||
|
Subscription subscriptionTemp = myClient.read(Subscription.class, subscription2.getId());
|
||||||
|
assertNotNull(subscriptionTemp);
|
||||||
|
subscriptionTemp.setTopic(subscription1.getTopic());
|
||||||
|
updateResource(subscriptionTemp, false);
|
||||||
|
|
||||||
|
Observation observation2 = sendObservationExpectDelivery();
|
||||||
|
|
||||||
|
assertReceivedTransactionCount(3);
|
||||||
|
|
||||||
|
deleteSubscription(subscription2);
|
||||||
|
|
||||||
|
Observation observationTemp3 = sendObservationExpectDelivery();
|
||||||
|
|
||||||
|
// Should see only one subscription notification
|
||||||
|
assertReceivedTransactionCount(4);
|
||||||
|
|
||||||
|
Observation observation3 = myClient.read(Observation.class, observationTemp3.getId());
|
||||||
|
CodeableConcept codeableConcept = new CodeableConcept();
|
||||||
|
observation3.setCode(codeableConcept);
|
||||||
|
Coding coding = codeableConcept.addCoding();
|
||||||
|
coding.setCode(OBS_CODE + "111");
|
||||||
|
coding.setSystem("SNOMED-CT");
|
||||||
|
updateResource(observation3, false);
|
||||||
|
|
||||||
|
// Should see no subscription notification
|
||||||
|
assertReceivedTransactionCount(4);
|
||||||
|
|
||||||
|
Observation observation3a = myClient.read(Observation.class, observationTemp3.getId());
|
||||||
|
|
||||||
|
CodeableConcept codeableConcept1 = new CodeableConcept();
|
||||||
|
observation3a.setCode(codeableConcept1);
|
||||||
|
Coding coding1 = codeableConcept1.addCoding();
|
||||||
|
coding1.setCode(OBS_CODE);
|
||||||
|
coding1.setSystem("SNOMED-CT");
|
||||||
|
updateResource(observation3a, true);
|
||||||
|
|
||||||
|
// Should see only one subscription notification
|
||||||
|
assertReceivedTransactionCount(5);
|
||||||
|
|
||||||
|
assertFalse(subscription1.getId().equals(subscription2.getId()));
|
||||||
|
assertFalse(sentObservation1.getId().isEmpty());
|
||||||
|
assertFalse(observation2.getId().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSubscription(Subscription subscription2) throws InterruptedException {
|
||||||
|
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
|
||||||
|
myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute();
|
||||||
|
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertReceivedTransactionCount(int theExpected) {
|
||||||
|
if (getSystemProviderCount() != theExpected) {
|
||||||
|
String list = getReceivedObservations().stream()
|
||||||
|
.map(t -> t.getIdElement().toUnqualifiedVersionless().getValue() + " " + t.getCode().getCodingFirstRep().getCode())
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
throw new AssertionError("Expected " + theExpected + " transactions, have " + getSystemProviderCount() + ": " + list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionApplicationJsonDatabase() throws Exception {
|
||||||
|
// Same test as above, but now run it using database matching
|
||||||
|
myStorageSettings.setEnableInMemorySubscriptionMatching(false);
|
||||||
|
testRestHookSubscriptionApplicationJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Subscription createTopicSubscription(String theTopicUrlSuffix) throws InterruptedException {
|
||||||
|
// WIP STR5 will likely require matching TopicSubscription
|
||||||
|
Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + theTopicUrlSuffix, Constants.CT_FHIR_JSON_NEW);
|
||||||
|
|
||||||
|
return postSubscription(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubscriptionTriggerViaSubscription() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
// WIP STR5 will likely require matching TopicSubscription
|
||||||
|
Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW);
|
||||||
|
|
||||||
|
Subscription subscription = postSubscription(subscription1);
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
ourLog.info("** About to send observation");
|
||||||
|
|
||||||
|
Observation sentObservation = new Observation();
|
||||||
|
sentObservation.addIdentifier().setSystem("foo").setValue("bar1");
|
||||||
|
sentObservation.setId(IdType.newRandomUuid().getValue());
|
||||||
|
CodeableConcept codeableConcept = new CodeableConcept()
|
||||||
|
.addCoding(new Coding().setCode(OBS_CODE).setSystem("SNOMED-CT"));
|
||||||
|
sentObservation.setCode(codeableConcept);
|
||||||
|
sentObservation.setStatus(Enumerations.ObservationStatus.FINAL);
|
||||||
|
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.addIdentifier().setSystem("foo").setValue("bar2");
|
||||||
|
patient.setId(IdType.newRandomUuid().getValue());
|
||||||
|
patient.setActive(true);
|
||||||
|
sentObservation.getSubject().setReference(patient.getId());
|
||||||
|
|
||||||
|
Bundle requestBundle = new Bundle();
|
||||||
|
requestBundle.setType(Bundle.BundleType.TRANSACTION);
|
||||||
|
requestBundle.addEntry()
|
||||||
|
.setResource(sentObservation)
|
||||||
|
.setFullUrl(sentObservation.getId())
|
||||||
|
.getRequest()
|
||||||
|
.setUrl("Observation?identifier=foo|bar1")
|
||||||
|
.setMethod(Bundle.HTTPVerb.PUT);
|
||||||
|
requestBundle.addEntry()
|
||||||
|
.setResource(patient)
|
||||||
|
.setFullUrl(patient.getId())
|
||||||
|
.getRequest()
|
||||||
|
.setUrl("Patient?identifier=foo|bar2")
|
||||||
|
.setMethod(Bundle.HTTPVerb.PUT);
|
||||||
|
|
||||||
|
sendTransaction(requestBundle, true);
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
Observation receivedObs = assertBundleAndGetObservation(subscription, sentObservation);
|
||||||
|
assertEquals(Constants.CT_FHIR_XML_NEW, getLastSystemProviderContentType());
|
||||||
|
|
||||||
|
ourLog.debug("Observation content: {}", myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(receivedObs));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateSubscriptionToMatchLater() throws Exception {
|
||||||
|
SubscriptionTopic subscriptionTopic = createObservationSubscriptionTopic(OBS_CODE2);
|
||||||
|
waitForRegisteredSubscriptionTopicCount(1);
|
||||||
|
|
||||||
|
ourLog.info("** About to create non-matching subscription");
|
||||||
|
|
||||||
|
// WIP STR5 will likely require matching TopicSubscription
|
||||||
|
Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_XML_NEW);
|
||||||
|
|
||||||
|
Subscription subscription = postSubscription(subscription1);
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
ourLog.info("** About to send observation that wont match");
|
||||||
|
|
||||||
|
Observation observation1 = sendObservation(OBS_CODE, "SNOMED-CT", false);
|
||||||
|
assertReceivedTransactionCount(0);
|
||||||
|
|
||||||
|
ourLog.info("** About to update subscription topic");
|
||||||
|
SubscriptionTopic subscriptionTopicTemp = myClient.read(SubscriptionTopic.class, subscriptionTopic.getId());
|
||||||
|
assertNotNull(subscriptionTopicTemp);
|
||||||
|
setSubscriptionTopicCriteria(subscriptionTopicTemp, "Observation?code=SNOMED-CT|" + OBS_CODE);
|
||||||
|
updateResource(subscriptionTopicTemp, false);
|
||||||
|
|
||||||
|
ourLog.info("** About to send Observation 2");
|
||||||
|
Observation observation2 = sendObservationExpectDelivery();
|
||||||
|
|
||||||
|
// Should see a subscription notification this time
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
|
||||||
|
deleteSubscription(subscription);
|
||||||
|
|
||||||
|
Observation observationTemp3 = sendObservation(OBS_CODE, "SNOMED-CT", false);
|
||||||
|
|
||||||
|
// No more matches
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setSubscriptionTopicCriteria(SubscriptionTopic subscriptionTopicTemp, String theCriteria) {
|
||||||
|
subscriptionTopicTemp.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(theCriteria);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionApplicationXmlJson() throws Exception {
|
||||||
|
createObservationSubscriptionTopic(OBS_CODE);
|
||||||
|
createObservationSubscriptionTopic(OBS_CODE2);
|
||||||
|
waitForRegisteredSubscriptionTopicCount(2);
|
||||||
|
|
||||||
|
// WIP STR5 will likely require matching TopicSubscription
|
||||||
|
Subscription subscription3 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW);
|
||||||
|
|
||||||
|
Subscription subscription1 = postSubscription(subscription3);
|
||||||
|
// WIP STR5 will likely require matching TopicSubscription
|
||||||
|
Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_XML_NEW);
|
||||||
|
|
||||||
|
Subscription subscription2 = postSubscription(subscription);
|
||||||
|
waitForActivatedSubscriptionCount(2);
|
||||||
|
|
||||||
|
Observation observation1 = sendObservationExpectDelivery();
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
assertEquals(Constants.CT_FHIR_XML_NEW, getLastSystemProviderContentType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookTopicSubscriptionInvalidTopic() throws Exception {
|
||||||
|
try {
|
||||||
|
createTopicSubscription(OBS_CODE);
|
||||||
|
fail();
|
||||||
|
} catch (UnprocessableEntityException e) {
|
||||||
|
assertEquals("HTTP 422 Unprocessable Entity: " + Msg.code(2322) + "No SubscriptionTopic exists with topic: " + SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestHookSubscriptionTopicInvalidCriteria() throws Exception {
|
||||||
|
try {
|
||||||
|
createSubscriptionTopicWithCriteria("Observation?codeeeee=SNOMED-CT");
|
||||||
|
} catch (UnprocessableEntityException e) {
|
||||||
|
assertEquals("HTTP 422 Unprocessable Entity: " + Msg.code(2339) + "Invalid SubscriptionTopic criteria 'Observation?codeeeee=SNOMED-CT' in SubscriptionTopic.resourceTrigger.queryCriteria.current: HAPI-0488: Failed to parse match URL[Observation?codeeeee=SNOMED-CT] - Resource type Observation does not have a parameter with name: codeeeee", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private SubscriptionTopic createSubscriptionTopicWithCriteria(String theCriteria) throws InterruptedException {
|
||||||
|
SubscriptionTopic subscriptionTopic = buildSubscriptionTopic(CUSTOM_URL);
|
||||||
|
setSubscriptionTopicCriteria(subscriptionTopic, theCriteria);
|
||||||
|
return createSubscriptionTopic(subscriptionTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubscriptionWithHeaders() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
// Add some headers, and we'll also turn back to requested status for fun
|
||||||
|
Subscription subscription = createTopicSubscription();
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
subscription.addHeader("X-Foo: FOO");
|
||||||
|
subscription.addHeader("X-Bar: BAR");
|
||||||
|
updateResource(subscription, false);
|
||||||
|
|
||||||
|
Observation sentObservation = sendObservationExpectDelivery();
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
Observation receivedObservation = assertBundleAndGetObservation(subscription, sentObservation);
|
||||||
|
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType());
|
||||||
|
|
||||||
|
assertThat(getLastSystemProviderHeaders(), hasItem("X-Foo: FOO"));
|
||||||
|
assertThat(getLastSystemProviderHeaders(), hasItem("X-Bar: BAR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDisableSubscription() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
Subscription subscription = createTopicSubscription();
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
Observation sentObservation = sendObservationExpectDelivery();
|
||||||
|
|
||||||
|
// Should see 1 subscription notification
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
Observation receivedObservation = assertBundleAndGetObservation(subscription, sentObservation);
|
||||||
|
|
||||||
|
// Disable
|
||||||
|
subscription.setStatus(Enumerations.SubscriptionStatusCodes.OFF);
|
||||||
|
updateResource(subscription, false);
|
||||||
|
|
||||||
|
// Send another observation
|
||||||
|
sendObservation(OBS_CODE, "SNOMED-CT", false);
|
||||||
|
|
||||||
|
// Should see no new delivery
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidProvenanceParam() {
|
||||||
|
assertThrows(UnprocessableEntityException.class, () -> {
|
||||||
|
String criteriabad = "Provenance?foo=http://hl7.org/fhir/v3/DocumentCompletion%7CAU";
|
||||||
|
createSubscriptionTopicWithCriteria(criteriabad);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidProcedureRequestParam() {
|
||||||
|
assertThrows(UnprocessableEntityException.class, () -> {
|
||||||
|
String criteriabad = "ProcedureRequest?intent=instance-order&category=Laboratory";
|
||||||
|
createSubscriptionTopicWithCriteria(criteriabad);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidBodySiteParam() {
|
||||||
|
assertThrows(UnprocessableEntityException.class, () -> {
|
||||||
|
String criteriabad = "BodySite?accessType=Catheter";
|
||||||
|
createSubscriptionTopicWithCriteria(criteriabad);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGoodSubscriptionPersists() throws Exception {
|
||||||
|
createSubscriptionTopic();
|
||||||
|
|
||||||
|
assertEquals(0, subscriptionCount());
|
||||||
|
Subscription subscription = createTopicSubscription();
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
assertEquals(1, subscriptionCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int subscriptionCount() {
|
||||||
|
IBaseBundle found = myClient.search().forResource(Subscription.class).cacheControl(new CacheControlDirective().setNoCache(true)).execute();
|
||||||
|
return toUnqualifiedVersionlessIdValues(found).size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubscriptionTopicWithNoStatusIsRejected() throws InterruptedException {
|
||||||
|
SubscriptionTopic subscriptionTopic = buildSubscriptionTopic(OBS_CODE);
|
||||||
|
subscriptionTopic.setStatus(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
createSubscriptionTopic(subscriptionTopic);
|
||||||
|
fail();
|
||||||
|
} catch (UnprocessableEntityException e) {
|
||||||
|
assertThat(e.getMessage(), containsString("Can not process submitted SubscriptionTopic - SubscriptionTopic.status must be populated on this server"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBadSubscriptionTopicDoesntPersist() throws InterruptedException {
|
||||||
|
assertEquals(0, subscriptionCount());
|
||||||
|
String criteriaBad = "BodySite?accessType=Catheter";
|
||||||
|
try {
|
||||||
|
createSubscriptionTopicWithCriteria(criteriaBad);
|
||||||
|
} catch (UnprocessableEntityException e) {
|
||||||
|
ourLog.info("Expected exception", e);
|
||||||
|
}
|
||||||
|
assertEquals(0, subscriptionCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCustomSearchParam() throws Exception {
|
||||||
|
String criteria = "Observation?accessType=Catheter,PD%20Catheter";
|
||||||
|
|
||||||
|
SearchParameter sp = new SearchParameter();
|
||||||
|
sp.addBase("Observation");
|
||||||
|
sp.setCode("accessType");
|
||||||
|
sp.setType(Enumerations.SearchParamType.TOKEN);
|
||||||
|
sp.setExpression("Observation.extension('Observation#accessType')");
|
||||||
|
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||||
|
mySearchParameterDao.create(sp);
|
||||||
|
mySearchParamRegistry.forceRefresh();
|
||||||
|
createSubscriptionTopicWithCriteria(criteria);
|
||||||
|
waitForRegisteredSubscriptionTopicCount(1);
|
||||||
|
|
||||||
|
Subscription subscription = createTopicSubscription(CUSTOM_URL);
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
|
||||||
|
{
|
||||||
|
Observation observation = new Observation();
|
||||||
|
observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("Catheter"));
|
||||||
|
createResource(observation, true);
|
||||||
|
assertReceivedTransactionCount(1);
|
||||||
|
assertBundleAndGetObservation(subscription, observation);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Observation observation = new Observation();
|
||||||
|
observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("PD Catheter"));
|
||||||
|
createResource(observation, true);
|
||||||
|
assertReceivedTransactionCount(2);
|
||||||
|
assertBundleAndGetObservation(subscription, observation);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Observation observation = new Observation();
|
||||||
|
createResource(observation, false);
|
||||||
|
assertReceivedTransactionCount(2);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Observation observation = new Observation();
|
||||||
|
observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("XXX"));
|
||||||
|
createResource(observation, false);
|
||||||
|
assertReceivedTransactionCount(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Observation assertBundleAndGetObservation(Subscription subscription, Observation sentObservation) {
|
||||||
|
Bundle receivedBundle = getLastSystemProviderBundle();
|
||||||
|
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, receivedBundle);
|
||||||
|
assertEquals(2, resources.size());
|
||||||
|
|
||||||
|
SubscriptionStatus ss = (SubscriptionStatus) resources.get(0);
|
||||||
|
validateSubscriptionStatus(subscription, sentObservation, ss);
|
||||||
|
|
||||||
|
return (Observation) resources.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubscriptionTopic createObservationSubscriptionTopic(String theCode) throws InterruptedException {
|
||||||
|
SubscriptionTopic subscriptionTopic = buildSubscriptionTopic(theCode);
|
||||||
|
return createSubscriptionTopic(subscriptionTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static SubscriptionTopic buildSubscriptionTopic(String theCode) {
|
||||||
|
SubscriptionTopic retval = new SubscriptionTopic();
|
||||||
|
retval.setUrl(SUBSCRIPTION_TOPIC_TEST_URL+ theCode);
|
||||||
|
retval.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||||
|
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = retval.addResourceTrigger();
|
||||||
|
trigger.setResource("Observation");
|
||||||
|
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.CREATE);
|
||||||
|
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
|
||||||
|
SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent queryCriteria = trigger.getQueryCriteria();
|
||||||
|
queryCriteria.setCurrent("Observation?code=SNOMED-CT|" + theCode);
|
||||||
|
queryCriteria.setRequireBoth(false);
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Observation sendObservation(String theCode, String theSystem, boolean theExpectDelivery) throws InterruptedException {
|
||||||
|
Observation observation = new Observation();
|
||||||
|
CodeableConcept codeableConcept = new CodeableConcept();
|
||||||
|
observation.setCode(codeableConcept);
|
||||||
|
observation.getIdentifierFirstRep().setSystem("foo").setValue("1");
|
||||||
|
Coding coding = codeableConcept.addCoding();
|
||||||
|
coding.setCode(theCode);
|
||||||
|
coding.setSystem(theSystem);
|
||||||
|
|
||||||
|
observation.setStatus(Enumerations.ObservationStatus.FINAL);
|
||||||
|
|
||||||
|
IIdType id = createResource(observation, theExpectDelivery);
|
||||||
|
observation.setId(id);
|
||||||
|
|
||||||
|
return observation;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,991 +0,0 @@
|
||||||
package ca.uhn.fhir.jpa.subscription.resthook;
|
|
||||||
|
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
|
||||||
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR5Test;
|
|
||||||
import ca.uhn.fhir.jpa.test.util.StoppableSubscriptionDeliveringRestHookSubscriber;
|
|
||||||
import ca.uhn.fhir.rest.api.CacheControlDirective;
|
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
|
||||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
|
||||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
|
||||||
import ca.uhn.fhir.util.HapiExtensions;
|
|
||||||
import org.hamcrest.MatcherAssert;
|
|
||||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
|
||||||
import org.hl7.fhir.r5.model.BooleanType;
|
|
||||||
import org.hl7.fhir.r5.model.Bundle;
|
|
||||||
import org.hl7.fhir.r5.model.CodeableConcept;
|
|
||||||
import org.hl7.fhir.r5.model.Coding;
|
|
||||||
import org.hl7.fhir.r5.model.Enumerations;
|
|
||||||
import org.hl7.fhir.r5.model.IdType;
|
|
||||||
import org.hl7.fhir.r5.model.Observation;
|
|
||||||
import org.hl7.fhir.r5.model.Patient;
|
|
||||||
import org.hl7.fhir.r5.model.SearchParameter;
|
|
||||||
import org.hl7.fhir.r5.model.Subscription;
|
|
||||||
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
import static org.hamcrest.Matchers.containsString;
|
|
||||||
import static org.hamcrest.Matchers.hasItem;
|
|
||||||
import static org.hamcrest.Matchers.matchesPattern;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the rest-hook subscriptions
|
|
||||||
*/
|
|
||||||
public class RestHookTestR5Test extends BaseSubscriptionsR5Test {
|
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(RestHookTestR5Test.class);
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
StoppableSubscriptionDeliveringRestHookSubscriber myStoppableSubscriptionDeliveringRestHookSubscriber;
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
public void cleanupStoppableSubscriptionDeliveringRestHookSubscriber() {
|
|
||||||
ourLog.info("@AfterEach");
|
|
||||||
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(null);
|
|
||||||
myStoppableSubscriptionDeliveringRestHookSubscriber.unPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRestHookSubscriptionApplicationFhirJson() throws Exception {
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
|
||||||
createSubscription(criteria2, payload);
|
|
||||||
waitForActivatedSubscriptionCount(2);
|
|
||||||
|
|
||||||
sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testUpdatesHaveCorrectMetadata() throws Exception {
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?";
|
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send version 1
|
|
||||||
*/
|
|
||||||
|
|
||||||
Observation obs = sendObservation(code, "SNOMED-CT");
|
|
||||||
obs = myObservationDao.read(obs.getIdElement().toUnqualifiedVersionless());
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
int idx = 0;
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(idx));
|
|
||||||
Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdElement().getVersionIdPart());
|
|
||||||
Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getVersionId());
|
|
||||||
Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString());
|
|
||||||
Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString());
|
|
||||||
Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdentifierFirstRep().getValue());
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send version 2
|
|
||||||
*/
|
|
||||||
|
|
||||||
obs.getIdentifierFirstRep().setSystem("foo").setValue("2");
|
|
||||||
myObservationDao.update(obs);
|
|
||||||
obs = myObservationDao.read(obs.getIdElement().toUnqualifiedVersionless());
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
idx++;
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(idx));
|
|
||||||
Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdElement().getVersionIdPart());
|
|
||||||
Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getVersionId());
|
|
||||||
Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString());
|
|
||||||
Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString());
|
|
||||||
Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdentifierFirstRep().getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@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(Enumerations.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, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
MatcherAssert.assertThat(BaseSubscriptionsR5Test.ourUpdatedObservations.get(0).getSubject().getReference(), matchesPattern("Patient/[0-9]+"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception {
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?";
|
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send version 1
|
|
||||||
*/
|
|
||||||
|
|
||||||
Observation observation = new Observation();
|
|
||||||
observation.getIdentifierFirstRep().setSystem("foo").setValue("1");
|
|
||||||
observation.getCode().addCoding().setCode(code).setSystem("SNOMED-CT");
|
|
||||||
observation.setStatus(Enumerations.ObservationStatus.FINAL);
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.setType(Bundle.BundleType.TRANSACTION);
|
|
||||||
bundle.addEntry().setResource(observation).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Observation");
|
|
||||||
Bundle responseBundle = mySystemDao.transaction(null, bundle);
|
|
||||||
|
|
||||||
Observation obs = myObservationDao.read(new IdType(responseBundle.getEntry().get(0).getResponse().getLocation()));
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
int idx = 0;
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(idx));
|
|
||||||
Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdElement().getVersionIdPart());
|
|
||||||
Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getVersionId());
|
|
||||||
Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString());
|
|
||||||
Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdentifierFirstRep().getValue());
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send version 2
|
|
||||||
*/
|
|
||||||
|
|
||||||
observation = new Observation();
|
|
||||||
observation.setId(obs.getId());
|
|
||||||
observation.getIdentifierFirstRep().setSystem("foo").setValue("2");
|
|
||||||
observation.getCode().addCoding().setCode(code).setSystem("SNOMED-CT");
|
|
||||||
observation.setStatus(Enumerations.ObservationStatus.FINAL);
|
|
||||||
bundle = new Bundle();
|
|
||||||
bundle.setType(Bundle.BundleType.TRANSACTION);
|
|
||||||
bundle.addEntry().setResource(observation).getRequest().setMethod(Bundle.HTTPVerb.PUT).setUrl(obs.getIdElement().toUnqualifiedVersionless().getValue());
|
|
||||||
mySystemDao.transaction(null, bundle);
|
|
||||||
obs = myObservationDao.read(obs.getIdElement().toUnqualifiedVersionless());
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
idx++;
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(idx));
|
|
||||||
Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdElement().getVersionIdPart());
|
|
||||||
Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getVersionId());
|
|
||||||
Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString());
|
|
||||||
Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdentifierFirstRep().getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRepeatedDeliveries() throws Exception {
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?";
|
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
|
|
||||||
for (int i = 0; i < 100; i++) {
|
|
||||||
Observation observation = new Observation();
|
|
||||||
observation.getIdentifierFirstRep().setSystem("foo").setValue("ID" + i);
|
|
||||||
observation.getCode().addCoding().setCode(code).setSystem("SNOMED-CT");
|
|
||||||
observation.setStatus(Enumerations.ObservationStatus.FINAL);
|
|
||||||
myObservationDao.create(observation);
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForSize(100, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testActiveSubscriptionShouldntReActivate() throws Exception {
|
|
||||||
String criteria = "Observation?code=111111111&_format=xml";
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
createSubscription(criteria, payload);
|
|
||||||
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
for (int i = 0; i < 5; i++) {
|
|
||||||
int changes = this.mySubscriptionLoader.doSyncSubscriptionsForUnitTest();
|
|
||||||
assertEquals(0, changes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRestHookSubscriptionNoopUpdateDoesntTriggerNewDelivery() throws Exception {
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
|
||||||
createSubscription(criteria2, payload);
|
|
||||||
waitForActivatedSubscriptionCount(2);
|
|
||||||
|
|
||||||
Observation obs = sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0));
|
|
||||||
|
|
||||||
// Send an update with no changes
|
|
||||||
obs.setId(obs.getIdElement().toUnqualifiedVersionless());
|
|
||||||
myClient.update().resource(obs).execute();
|
|
||||||
|
|
||||||
// Should be no further deliveries
|
|
||||||
Thread.sleep(1000);
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRestHookSubscriptionApplicationJsonDisableVersionIdInDelivery() throws Exception {
|
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
|
|
||||||
waitForActivatedSubscriptionCount(0);
|
|
||||||
Subscription subscription1 = createSubscription(criteria1, payload);
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
|
|
||||||
|
|
||||||
ourLog.info("** About to send observation");
|
|
||||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0));
|
|
||||||
|
|
||||||
IdType idElement = BaseSubscriptionsR5Test.ourUpdatedObservations.get(0).getIdElement();
|
|
||||||
assertEquals(observation1.getIdElement().getIdPart(), idElement.getIdPart());
|
|
||||||
// VersionId is present
|
|
||||||
assertEquals(observation1.getIdElement().getVersionIdPart(), idElement.getVersionIdPart());
|
|
||||||
|
|
||||||
subscription1
|
|
||||||
.addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS, new BooleanType("true"));
|
|
||||||
ourLog.info("** About to update subscription");
|
|
||||||
|
|
||||||
int modCount = myCountingInterceptor.getSentCount("Subscription");
|
|
||||||
ourLog.info("** About to send another...");
|
|
||||||
myClient.update().resource(subscription1).execute();
|
|
||||||
waitForSize(modCount + 2, () -> myCountingInterceptor.getSentCount("Subscription"), () -> myCountingInterceptor.toString());
|
|
||||||
|
|
||||||
ourLog.info("** About to send observation");
|
|
||||||
Observation observation2 = sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(1));
|
|
||||||
|
|
||||||
idElement = BaseSubscriptionsR5Test.ourUpdatedObservations.get(1).getIdElement();
|
|
||||||
assertEquals(observation2.getIdElement().getIdPart(), idElement.getIdPart());
|
|
||||||
// Now VersionId is stripped
|
|
||||||
assertEquals(null, idElement.getVersionIdPart());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRestHookSubscriptionDoesntGetLatestVersionByDefault() throws Exception {
|
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
|
|
||||||
waitForActivatedSubscriptionCount(0);
|
|
||||||
createSubscription(criteria1, payload);
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
|
|
||||||
myStoppableSubscriptionDeliveringRestHookSubscriber.pause();
|
|
||||||
final CountDownLatch countDownLatch = new CountDownLatch(1);
|
|
||||||
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch);
|
|
||||||
|
|
||||||
ourLog.info("** About to send observation");
|
|
||||||
Observation observation = sendObservation(code, "SNOMED-CT");
|
|
||||||
assertEquals("1", observation.getIdElement().getVersionIdPart());
|
|
||||||
assertNull(observation.getNoteFirstRep().getText());
|
|
||||||
|
|
||||||
observation.getNoteFirstRep().setText("changed");
|
|
||||||
MethodOutcome methodOutcome = myClient.update().resource(observation).execute();
|
|
||||||
assertEquals("2", methodOutcome.getId().getVersionIdPart());
|
|
||||||
assertEquals("changed", observation.getNoteFirstRep().getText());
|
|
||||||
|
|
||||||
// Wait for our two delivery channel threads to be paused
|
|
||||||
assertTrue(countDownLatch.await(5L, TimeUnit.SECONDS));
|
|
||||||
// Open the floodgates!
|
|
||||||
myStoppableSubscriptionDeliveringRestHookSubscriber.unPause();
|
|
||||||
|
|
||||||
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
Observation observation1 = BaseSubscriptionsR5Test.ourUpdatedObservations.stream().filter(t->t.getIdElement().getVersionIdPart().equals("1")).findFirst().orElseThrow(()->new IllegalStateException());
|
|
||||||
Observation observation2 = BaseSubscriptionsR5Test.ourUpdatedObservations.stream().filter(t->t.getIdElement().getVersionIdPart().equals("2")).findFirst().orElseThrow(()->new IllegalStateException());
|
|
||||||
|
|
||||||
assertEquals("1", observation1.getIdElement().getVersionIdPart());
|
|
||||||
assertNull(observation1.getNoteFirstRep().getText());
|
|
||||||
assertEquals("2", observation2.getIdElement().getVersionIdPart());
|
|
||||||
assertEquals("changed", observation2.getNoteFirstRep().getText());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRestHookSubscriptionGetsLatestVersionWithFlag() throws Exception {
|
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
|
|
||||||
waitForActivatedSubscriptionCount(0);
|
|
||||||
|
|
||||||
Subscription subscription = newSubscription(criteria1, payload);
|
|
||||||
subscription
|
|
||||||
.addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true"));
|
|
||||||
myClient.create().resource(subscription).execute();
|
|
||||||
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
|
|
||||||
myStoppableSubscriptionDeliveringRestHookSubscriber.pause();
|
|
||||||
final CountDownLatch countDownLatch = new CountDownLatch(1);
|
|
||||||
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch);
|
|
||||||
|
|
||||||
ourLog.info("** About to send observation");
|
|
||||||
Observation observation = sendObservation(code, "SNOMED-CT");
|
|
||||||
assertEquals("1", observation.getIdElement().getVersionIdPart());
|
|
||||||
assertNull(observation.getNoteFirstRep().getText());
|
|
||||||
|
|
||||||
observation.getNoteFirstRep().setText("changed");
|
|
||||||
MethodOutcome methodOutcome = myClient.update().resource(observation).execute();
|
|
||||||
assertEquals("2", methodOutcome.getId().getVersionIdPart());
|
|
||||||
assertEquals("changed", observation.getNoteFirstRep().getText());
|
|
||||||
|
|
||||||
// Wait for our two delivery channel threads to be paused
|
|
||||||
assertTrue(countDownLatch.await(5L, TimeUnit.SECONDS));
|
|
||||||
// Open the floodgates!
|
|
||||||
myStoppableSubscriptionDeliveringRestHookSubscriber.unPause();
|
|
||||||
|
|
||||||
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
Observation observation1 = BaseSubscriptionsR5Test.ourUpdatedObservations.get(0);
|
|
||||||
Observation observation2 = BaseSubscriptionsR5Test.ourUpdatedObservations.get(1);
|
|
||||||
|
|
||||||
assertEquals("2", observation1.getIdElement().getVersionIdPart());
|
|
||||||
assertEquals("changed", observation1.getNoteFirstRep().getText());
|
|
||||||
assertEquals("2", observation2.getIdElement().getVersionIdPart());
|
|
||||||
assertEquals("changed", observation2.getNoteFirstRep().getText());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRestHookSubscriptionApplicationJson() throws Exception {
|
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
|
||||||
|
|
||||||
Subscription subscription1 = createSubscription(criteria1, payload);
|
|
||||||
Subscription subscription2 = createSubscription(criteria2, payload);
|
|
||||||
waitForActivatedSubscriptionCount(2);
|
|
||||||
|
|
||||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0));
|
|
||||||
|
|
||||||
Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(0).getIdElement().getVersionIdPart());
|
|
||||||
|
|
||||||
Subscription subscriptionTemp = myClient.read(Subscription.class, subscription2.getId());
|
|
||||||
assertNotNull(subscriptionTemp);
|
|
||||||
|
|
||||||
SubscriptionTopic topic = (SubscriptionTopic) subscriptionTemp.getContained().get(0);
|
|
||||||
topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(criteria1);
|
|
||||||
|
|
||||||
myClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
Observation observation2 = sendObservation(code, "SNOMED-CT");
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
// Should see two subscription notifications
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(3, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute();
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
Observation observationTemp3 = sendObservation(code, "SNOMED-CT");
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
// Should see only one subscription notification
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
Observation observation3 = myClient.read(Observation.class, observationTemp3.getId());
|
|
||||||
CodeableConcept codeableConcept = new CodeableConcept();
|
|
||||||
observation3.setCode(codeableConcept);
|
|
||||||
Coding coding = codeableConcept.addCoding();
|
|
||||||
coding.setCode(code + "111");
|
|
||||||
coding.setSystem("SNOMED-CT");
|
|
||||||
myClient.update().resource(observation3).withId(observation3.getIdElement()).execute();
|
|
||||||
|
|
||||||
// Should see no subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
Observation observation3a = myClient.read(Observation.class, observationTemp3.getId());
|
|
||||||
|
|
||||||
CodeableConcept codeableConcept1 = new CodeableConcept();
|
|
||||||
observation3a.setCode(codeableConcept1);
|
|
||||||
Coding coding1 = codeableConcept1.addCoding();
|
|
||||||
coding1.setCode(code);
|
|
||||||
coding1.setSystem("SNOMED-CT");
|
|
||||||
myClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute();
|
|
||||||
|
|
||||||
// Should see only one subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(5, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
assertFalse(subscription1.getId().equals(subscription2.getId()));
|
|
||||||
assertFalse(observation1.getId().isEmpty());
|
|
||||||
assertFalse(observation2.getId().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRestHookSubscriptionApplicationJsonDatabase() throws Exception {
|
|
||||||
// Same test as above, but now run it using database matching
|
|
||||||
myStorageSettings.setEnableInMemorySubscriptionMatching(false);
|
|
||||||
String payload = "application/json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
|
||||||
|
|
||||||
Subscription subscription1 = createSubscription(criteria1, payload);
|
|
||||||
Subscription subscription2 = createSubscription(criteria2, payload);
|
|
||||||
waitForActivatedSubscriptionCount(2);
|
|
||||||
|
|
||||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0));
|
|
||||||
|
|
||||||
Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(0).getIdElement().getVersionIdPart());
|
|
||||||
|
|
||||||
Subscription subscriptionTemp = myClient.read(Subscription.class, subscription2.getId());
|
|
||||||
assertNotNull(subscriptionTemp);
|
|
||||||
|
|
||||||
SubscriptionTopic topic = (SubscriptionTopic) subscriptionTemp.getContained().get(0);
|
|
||||||
topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(criteria1);
|
|
||||||
|
|
||||||
myClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
Observation observation2 = sendObservation(code, "SNOMED-CT");
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
// Should see two subscription notifications
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(3, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute();
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
Observation observationTemp3 = sendObservation(code, "SNOMED-CT");
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
// Should see only one subscription notification
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
Observation observation3 = myClient.read(Observation.class, observationTemp3.getId());
|
|
||||||
CodeableConcept codeableConcept = new CodeableConcept();
|
|
||||||
observation3.setCode(codeableConcept);
|
|
||||||
Coding coding = codeableConcept.addCoding();
|
|
||||||
coding.setCode(code + "111");
|
|
||||||
coding.setSystem("SNOMED-CT");
|
|
||||||
myClient.update().resource(observation3).withId(observation3.getIdElement()).execute();
|
|
||||||
|
|
||||||
// Should see no subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
Observation observation3a = myClient.read(Observation.class, observationTemp3.getId());
|
|
||||||
|
|
||||||
CodeableConcept codeableConcept1 = new CodeableConcept();
|
|
||||||
observation3a.setCode(codeableConcept1);
|
|
||||||
Coding coding1 = codeableConcept1.addCoding();
|
|
||||||
coding1.setCode(code);
|
|
||||||
coding1.setSystem("SNOMED-CT");
|
|
||||||
myClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute();
|
|
||||||
|
|
||||||
// Should see only one subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(5, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
assertFalse(subscription1.getId().equals(subscription2.getId()));
|
|
||||||
assertFalse(observation1.getId().isEmpty());
|
|
||||||
assertFalse(observation2.getId().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRestHookSubscriptionApplicationXml() throws Exception {
|
|
||||||
String payload = "application/xml";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
|
||||||
|
|
||||||
Subscription subscription1 = createSubscription(criteria1, payload);
|
|
||||||
Subscription subscription2 = createSubscription(criteria2, payload);
|
|
||||||
waitForActivatedSubscriptionCount(2);
|
|
||||||
|
|
||||||
ourLog.info("** About to send observation");
|
|
||||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourContentTypes);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_XML_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0));
|
|
||||||
|
|
||||||
Subscription subscriptionTemp = myClient.read(Subscription.class, subscription2.getId());
|
|
||||||
assertNotNull(subscriptionTemp);
|
|
||||||
SubscriptionTopic topic = (SubscriptionTopic) subscriptionTemp.getContained().get(0);
|
|
||||||
topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(criteria1);
|
|
||||||
|
|
||||||
myClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
Observation observation2 = sendObservation(code, "SNOMED-CT");
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
// Should see two subscription notifications
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(3, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute();
|
|
||||||
|
|
||||||
Observation observationTemp3 = sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Should see only one subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
Observation observation3 = myClient.read(Observation.class, observationTemp3.getId());
|
|
||||||
CodeableConcept codeableConcept = new CodeableConcept();
|
|
||||||
observation3.setCode(codeableConcept);
|
|
||||||
Coding coding = codeableConcept.addCoding();
|
|
||||||
coding.setCode(code + "111");
|
|
||||||
coding.setSystem("SNOMED-CT");
|
|
||||||
myClient.update().resource(observation3).withId(observation3.getIdElement()).execute();
|
|
||||||
|
|
||||||
// Should see no subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
Observation observation3a = myClient.read(Observation.class, observationTemp3.getId());
|
|
||||||
|
|
||||||
CodeableConcept codeableConcept1 = new CodeableConcept();
|
|
||||||
observation3a.setCode(codeableConcept1);
|
|
||||||
Coding coding1 = codeableConcept1.addCoding();
|
|
||||||
coding1.setCode(code);
|
|
||||||
coding1.setSystem("SNOMED-CT");
|
|
||||||
myClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute();
|
|
||||||
|
|
||||||
// Should see only one subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(5, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
assertFalse(subscription1.getId().equals(subscription2.getId()));
|
|
||||||
assertFalse(observation1.getId().isEmpty());
|
|
||||||
assertFalse(observation2.getId().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSubscriptionTriggerViaSubscription() throws Exception {
|
|
||||||
String payload = "application/xml";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
|
|
||||||
createSubscription(criteria1, payload);
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
|
|
||||||
ourLog.info("** About to send observation");
|
|
||||||
|
|
||||||
Observation observation = new Observation();
|
|
||||||
observation.addIdentifier().setSystem("foo").setValue("bar1");
|
|
||||||
observation.setId(IdType.newRandomUuid().getValue());
|
|
||||||
CodeableConcept codeableConcept = new CodeableConcept()
|
|
||||||
.addCoding(new Coding().setCode(code).setSystem("SNOMED-CT"));
|
|
||||||
observation.setCode(codeableConcept);
|
|
||||||
observation.setStatus(Enumerations.ObservationStatus.FINAL);
|
|
||||||
|
|
||||||
Patient patient = new Patient();
|
|
||||||
patient.addIdentifier().setSystem("foo").setValue("bar2");
|
|
||||||
patient.setId(IdType.newRandomUuid().getValue());
|
|
||||||
patient.setActive(true);
|
|
||||||
observation.getSubject().setReference(patient.getId());
|
|
||||||
|
|
||||||
Bundle requestBundle = new Bundle();
|
|
||||||
requestBundle.setType(Bundle.BundleType.TRANSACTION);
|
|
||||||
requestBundle.addEntry()
|
|
||||||
.setResource(observation)
|
|
||||||
.setFullUrl(observation.getId())
|
|
||||||
.getRequest()
|
|
||||||
.setUrl("Observation?identifier=foo|bar1")
|
|
||||||
.setMethod(Bundle.HTTPVerb.PUT);
|
|
||||||
requestBundle.addEntry()
|
|
||||||
.setResource(patient)
|
|
||||||
.setFullUrl(patient.getId())
|
|
||||||
.getRequest()
|
|
||||||
.setUrl("Patient?identifier=foo|bar2")
|
|
||||||
.setMethod(Bundle.HTTPVerb.PUT);
|
|
||||||
myClient.transaction().withBundle(requestBundle).execute();
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_XML_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0));
|
|
||||||
|
|
||||||
Observation obs = BaseSubscriptionsR5Test.ourUpdatedObservations.get(0);
|
|
||||||
ourLog.debug("Observation content: {}", myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(obs));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testUpdateSubscriptionToMatchLater() throws Exception {
|
|
||||||
String payload = "application/xml";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteriaBad = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
|
||||||
|
|
||||||
ourLog.info("** About to create non-matching subscription");
|
|
||||||
|
|
||||||
Subscription subscription2 = createSubscription(criteriaBad, payload);
|
|
||||||
|
|
||||||
ourLog.info("** About to send observation that wont match");
|
|
||||||
|
|
||||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Criteria didn't match, shouldn't see any updates
|
|
||||||
waitForQueueToDrain();
|
|
||||||
Thread.sleep(1000);
|
|
||||||
Assertions.assertEquals(0, BaseSubscriptionsR5Test.ourUpdatedObservations.size());
|
|
||||||
|
|
||||||
Subscription subscriptionTemp = myClient.read().resource(Subscription.class).withId(subscription2.getId()).execute();
|
|
||||||
assertNotNull(subscriptionTemp);
|
|
||||||
String criteriaGood = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
|
|
||||||
SubscriptionTopic topic = (SubscriptionTopic) subscriptionTemp.getContained().get(0);
|
|
||||||
topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(criteriaGood);
|
|
||||||
|
|
||||||
ourLog.info("** About to update subscription");
|
|
||||||
myClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
ourLog.info("** About to send Observation 2");
|
|
||||||
Observation observation2 = sendObservation(code, "SNOMED-CT");
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
// Should see a subscription notification this time
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute();
|
|
||||||
|
|
||||||
Observation observationTemp3 = sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// No more matches
|
|
||||||
Thread.sleep(1000);
|
|
||||||
Assertions.assertEquals(1, BaseSubscriptionsR5Test.ourUpdatedObservations.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRestHookSubscriptionApplicationXmlJson() throws Exception {
|
|
||||||
String payload = "application/fhir+xml";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
|
|
||||||
|
|
||||||
Subscription subscription1 = createSubscription(criteria1, payload);
|
|
||||||
Subscription subscription2 = createSubscription(criteria2, payload);
|
|
||||||
waitForActivatedSubscriptionCount(2);
|
|
||||||
|
|
||||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_XML_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRestHookSubscriptionInvalidCriteria() throws Exception {
|
|
||||||
String payload = "application/xml";
|
|
||||||
|
|
||||||
String criteria1 = "Observation?codeeeee=SNOMED-CT";
|
|
||||||
|
|
||||||
try {
|
|
||||||
createSubscription(criteria1, payload);
|
|
||||||
fail();
|
|
||||||
} catch (UnprocessableEntityException e) {
|
|
||||||
assertEquals("HTTP 422 Unprocessable Entity: " + Msg.code(9) + "Invalid subscription criteria submitted: Observation?codeeeee=SNOMED-CT " + Msg.code(488) + "Failed to parse match URL[Observation?codeeeee=SNOMED-CT] - Resource type Observation does not have a parameter with name: codeeeee", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSubscriptionWithHeaders() throws Exception {
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
|
|
||||||
// Add some headers, and we'll also turn back to requested status for fun
|
|
||||||
Subscription subscription = createSubscription(criteria1, payload);
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
|
|
||||||
subscription.addHeader("X-Foo: FOO");
|
|
||||||
subscription.addHeader("X-Bar: BAR");
|
|
||||||
subscription.setStatus(Enumerations.SubscriptionStatusCodes.REQUESTED);
|
|
||||||
myClient.update().resource(subscription).execute();
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0));
|
|
||||||
assertThat(BaseSubscriptionsR5Test.ourHeaders, hasItem("X-Foo: FOO"));
|
|
||||||
assertThat(BaseSubscriptionsR5Test.ourHeaders, hasItem("X-Bar: BAR"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testDisableSubscription() throws Exception {
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
|
|
||||||
String code = "1000000050";
|
|
||||||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
|
|
||||||
|
|
||||||
Subscription subscription = createSubscription(criteria1, payload);
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
|
|
||||||
sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
// Disable
|
|
||||||
subscription.setStatus(Enumerations.SubscriptionStatusCodes.OFF);
|
|
||||||
myClient.update().resource(subscription).execute();
|
|
||||||
waitForQueueToDrain();
|
|
||||||
|
|
||||||
// Send another object
|
|
||||||
sendObservation(code, "SNOMED-CT");
|
|
||||||
|
|
||||||
// Should see 1 subscription notification
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations);
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInvalidProvenanceParam() {
|
|
||||||
assertThrows(UnprocessableEntityException.class, () -> {
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String criteriabad = "Provenance?foo=http://hl7.org/fhir/v3/DocumentCompletion%7CAU";
|
|
||||||
Subscription subscription = newSubscription(criteriabad, payload);
|
|
||||||
myClient.create().resource(subscription).execute();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInvalidProcedureRequestParam() {
|
|
||||||
assertThrows(UnprocessableEntityException.class, () -> {
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String criteriabad = "ProcedureRequest?intent=instance-order&category=Laboratory";
|
|
||||||
Subscription subscription = newSubscription(criteriabad, payload);
|
|
||||||
myClient.create().resource(subscription).execute();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInvalidBodySiteParam() {
|
|
||||||
assertThrows(UnprocessableEntityException.class, () -> {
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String criteriabad = "BodySite?accessType=Catheter";
|
|
||||||
Subscription subscription = newSubscription(criteriabad, payload);
|
|
||||||
myClient.create().resource(subscription).execute();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGoodSubscriptionPersists() {
|
|
||||||
assertEquals(0, subscriptionCount());
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String criteriaGood = "Patient?gender=male";
|
|
||||||
Subscription subscription = newSubscription(criteriaGood, payload);
|
|
||||||
myClient.create().resource(subscription).execute();
|
|
||||||
assertEquals(1, subscriptionCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
private int subscriptionCount() {
|
|
||||||
IBaseBundle found = myClient.search().forResource(Subscription.class).cacheControl(new CacheControlDirective().setNoCache(true)).execute();
|
|
||||||
return toUnqualifiedVersionlessIdValues(found).size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSubscriptionWithNoStatusIsRejected() {
|
|
||||||
Subscription subscription = newSubscription("Observation?", "application/json");
|
|
||||||
subscription.setStatus(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
myClient.create().resource(subscription).execute();
|
|
||||||
fail();
|
|
||||||
} catch (UnprocessableEntityException e) {
|
|
||||||
assertThat(e.getMessage(), containsString("Can not process submitted Subscription - Subscription.status must be populated on this server"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBadSubscriptionDoesntPersist() {
|
|
||||||
assertEquals(0, subscriptionCount());
|
|
||||||
String payload = "application/fhir+json";
|
|
||||||
String criteriaBad = "BodySite?accessType=Catheter";
|
|
||||||
Subscription subscription = newSubscription(criteriaBad, payload);
|
|
||||||
try {
|
|
||||||
myClient.create().resource(subscription).execute();
|
|
||||||
} catch (UnprocessableEntityException e) {
|
|
||||||
ourLog.info("Expected exception", e);
|
|
||||||
}
|
|
||||||
assertEquals(0, subscriptionCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCustomSearchParam() throws Exception {
|
|
||||||
String criteria = "Observation?accessType=Catheter,PD%20Catheter";
|
|
||||||
|
|
||||||
SearchParameter sp = new SearchParameter();
|
|
||||||
sp.addBase("Observation");
|
|
||||||
sp.setCode("accessType");
|
|
||||||
sp.setType(Enumerations.SearchParamType.TOKEN);
|
|
||||||
sp.setExpression("Observation.extension('Observation#accessType')");
|
|
||||||
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
|
||||||
mySearchParameterDao.create(sp);
|
|
||||||
mySearchParamRegistry.forceRefresh();
|
|
||||||
createSubscription(criteria, "application/json");
|
|
||||||
waitForActivatedSubscriptionCount(1);
|
|
||||||
|
|
||||||
{
|
|
||||||
Observation bodySite = new Observation();
|
|
||||||
bodySite.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("Catheter"));
|
|
||||||
MethodOutcome methodOutcome = myClient.create().resource(bodySite).execute();
|
|
||||||
assertEquals(true, methodOutcome.getCreated());
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
Observation observation = new Observation();
|
|
||||||
observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("PD Catheter"));
|
|
||||||
MethodOutcome methodOutcome = myClient.create().resource(observation).execute();
|
|
||||||
assertEquals(true, methodOutcome.getCreated());
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
Observation observation = new Observation();
|
|
||||||
MethodOutcome methodOutcome = myClient.create().resource(observation).execute();
|
|
||||||
assertEquals(true, methodOutcome.getCreated());
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
Observation observation = new Observation();
|
|
||||||
observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("XXX"));
|
|
||||||
MethodOutcome methodOutcome = myClient.create().resource(observation).execute();
|
|
||||||
assertEquals(true, methodOutcome.getCreated());
|
|
||||||
waitForQueueToDrain();
|
|
||||||
waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -34,6 +34,7 @@ import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -50,6 +51,8 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im
|
||||||
protected RequestPartitionId myPartitionId;
|
protected RequestPartitionId myPartitionId;
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
protected transient IBaseResource myPayloadDecoded;
|
protected transient IBaseResource myPayloadDecoded;
|
||||||
|
@JsonIgnore
|
||||||
|
protected transient String myPayloadType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
@ -126,6 +129,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public IBaseResource getNewPayload(FhirContext theCtx) {
|
public IBaseResource getNewPayload(FhirContext theCtx) {
|
||||||
if (myPayloadDecoded == null && isNotBlank(myPayload)) {
|
if (myPayloadDecoded == null && isNotBlank(myPayload)) {
|
||||||
myPayloadDecoded = theCtx.newJsonParser().parseResource(myPayload);
|
myPayloadDecoded = theCtx.newJsonParser().parseResource(myPayload);
|
||||||
|
@ -133,6 +137,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im
|
||||||
return myPayloadDecoded;
|
return myPayloadDecoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public IBaseResource getPayload(FhirContext theCtx) {
|
public IBaseResource getPayload(FhirContext theCtx) {
|
||||||
IBaseResource retVal = myPayloadDecoded;
|
IBaseResource retVal = myPayloadDecoded;
|
||||||
if (retVal == null && isNotBlank(myPayload)) {
|
if (retVal == null && isNotBlank(myPayload)) {
|
||||||
|
@ -143,6 +148,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
public String getPayloadString() {
|
public String getPayloadString() {
|
||||||
if (this.myPayload != null) {
|
if (this.myPayload != null) {
|
||||||
return this.myPayload;
|
return this.myPayload;
|
||||||
|
@ -228,5 +234,27 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im
|
||||||
return StringUtils.defaultString(super.getMessageKey(), myPayloadId);
|
return StringUtils.defaultString(super.getMessageKey(), myPayloadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasPayloadType(FhirContext theFhirContext, @Nonnull String theResourceName) {
|
||||||
|
if (myPayloadType == null) {
|
||||||
|
myPayloadType = getPayloadType(theFhirContext);
|
||||||
|
}
|
||||||
|
return theResourceName.equals(myPayloadType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getPayloadType(FhirContext theFhirContext) {
|
||||||
|
String retval = null;
|
||||||
|
IIdType payloadId = getPayloadId(theFhirContext);
|
||||||
|
if (payloadId != null) {
|
||||||
|
retval = payloadId.getResourceType();
|
||||||
|
}
|
||||||
|
if (isBlank(retval)) {
|
||||||
|
IBaseResource payload = getNewPayload(theFhirContext);
|
||||||
|
if (payload != null) {
|
||||||
|
retval = theFhirContext.getResourceType(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscriptionFilter;
|
||||||
import ca.uhn.fhir.model.api.BasePrimitive;
|
import ca.uhn.fhir.model.api.BasePrimitive;
|
||||||
import ca.uhn.fhir.model.api.ExtensionDt;
|
import ca.uhn.fhir.model.api.ExtensionDt;
|
||||||
import ca.uhn.fhir.model.dstu2.resource.Subscription;
|
import ca.uhn.fhir.model.dstu2.resource.Subscription;
|
||||||
|
@ -327,9 +328,6 @@ public class SubscriptionCanonicalizer {
|
||||||
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode()));
|
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode()));
|
||||||
}
|
}
|
||||||
setPartitionIdOnReturnValue(theSubscription, retVal);
|
setPartitionIdOnReturnValue(theSubscription, retVal);
|
||||||
retVal.setChannelType(getChannelType(subscription));
|
|
||||||
retVal.setCriteriaString(getCriteria(theSubscription));
|
|
||||||
retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
|
|
||||||
retVal.setHeaders(subscription.getChannel().getHeader());
|
retVal.setHeaders(subscription.getChannel().getHeader());
|
||||||
retVal.setChannelExtensions(extractExtension(subscription));
|
retVal.setChannelExtensions(extractExtension(subscription));
|
||||||
retVal.setIdElement(subscription.getIdElement());
|
retVal.setIdElement(subscription.getIdElement());
|
||||||
|
@ -344,6 +342,18 @@ public class SubscriptionCanonicalizer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (retVal.isTopicSubscription()) {
|
||||||
|
retVal.getTopicSubscription().setTopic(getCriteria(theSubscription));
|
||||||
|
retVal.getTopicSubscription().setContent(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE);
|
||||||
|
retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
|
||||||
|
retVal.setChannelType(getChannelType(subscription));
|
||||||
|
// WIP STR5 set other topic subscription fields
|
||||||
|
} else {
|
||||||
|
retVal.setCriteriaString(getCriteria(theSubscription));
|
||||||
|
retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
|
||||||
|
retVal.setChannelType(getChannelType(subscription));
|
||||||
|
}
|
||||||
|
|
||||||
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
||||||
String from;
|
String from;
|
||||||
String subjectTemplate;
|
String subjectTemplate;
|
||||||
|
@ -389,18 +399,10 @@ public class SubscriptionCanonicalizer {
|
||||||
private CanonicalSubscription canonicalizeR5(IBaseResource theSubscription) {
|
private CanonicalSubscription canonicalizeR5(IBaseResource theSubscription) {
|
||||||
org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription;
|
org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription;
|
||||||
|
|
||||||
// WIP STR5 now that we have SubscriptionTopic, rewrite this so that all R5 subscriptions are SubscriptionTopic
|
|
||||||
// subscriptions. This will require major rework of RestHookTestR5Test
|
|
||||||
|
|
||||||
CanonicalSubscription retVal = new CanonicalSubscription();
|
CanonicalSubscription retVal = new CanonicalSubscription();
|
||||||
Enumerations.SubscriptionStatusCodes status = subscription.getStatus();
|
|
||||||
if (status != null) {
|
|
||||||
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode()));
|
|
||||||
}
|
|
||||||
setPartitionIdOnReturnValue(theSubscription, retVal);
|
setPartitionIdOnReturnValue(theSubscription, retVal);
|
||||||
retVal.setChannelType(getChannelType(subscription));
|
|
||||||
retVal.setCriteriaString(getCriteria(theSubscription));
|
|
||||||
retVal.setEndpointUrl(subscription.getEndpoint());
|
|
||||||
retVal.setHeaders(subscription.getHeader());
|
retVal.setHeaders(subscription.getHeader());
|
||||||
retVal.setChannelExtensions(extractExtension(subscription));
|
retVal.setChannelExtensions(extractExtension(subscription));
|
||||||
retVal.setIdElement(subscription.getIdElement());
|
retVal.setIdElement(subscription.getIdElement());
|
||||||
|
@ -408,14 +410,40 @@ public class SubscriptionCanonicalizer {
|
||||||
retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
|
retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
|
||||||
retVal.setTags(extractTags(subscription));
|
retVal.setTags(extractTags(subscription));
|
||||||
|
|
||||||
|
List<org.hl7.fhir.r5.model.Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
|
||||||
|
if (topicExts.size() > 0) {
|
||||||
|
IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive();
|
||||||
|
if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) {
|
||||||
|
throw new PreconditionFailedException(Msg.code(2325) + "Topic reference must be an EventDefinition");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<org.hl7.fhir.r5.model.CanonicalType> profiles = subscription.getMeta().getProfile();
|
// All R5 subscriptions are topic subscriptions
|
||||||
for (org.hl7.fhir.r5.model.CanonicalType next : profiles) {
|
|
||||||
if (SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL.equals(next.getValueAsString())) {
|
|
||||||
retVal.setTopicSubscription(true);
|
retVal.setTopicSubscription(true);
|
||||||
|
|
||||||
|
Enumerations.SubscriptionStatusCodes status = subscription.getStatus();
|
||||||
|
if (status != null) {
|
||||||
|
// WIP STR5 do all the codes map?
|
||||||
|
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode()));
|
||||||
}
|
}
|
||||||
|
retVal.getTopicSubscription().setContent(subscription.getContent());
|
||||||
|
retVal.setEndpointUrl(subscription.getEndpoint());
|
||||||
|
retVal.getTopicSubscription().setTopic(subscription.getTopic());
|
||||||
|
retVal.setChannelType(getChannelType(subscription));
|
||||||
|
|
||||||
|
subscription.getFilterBy().forEach(filter -> {
|
||||||
|
retVal.getTopicSubscription().addFilter(convertFilter(filter));
|
||||||
|
});
|
||||||
|
|
||||||
|
retVal.getTopicSubscription().setHeartbeatPeriod(subscription.getHeartbeatPeriod());
|
||||||
|
retVal.getTopicSubscription().setMaxCount(subscription.getMaxCount());
|
||||||
|
|
||||||
|
setR5FlagsBasedOnChannelType(subscription, retVal);
|
||||||
|
|
||||||
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setR5FlagsBasedOnChannelType(org.hl7.fhir.r5.model.Subscription subscription, CanonicalSubscription retVal) {
|
||||||
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
||||||
String from;
|
String from;
|
||||||
String subjectTemplate;
|
String subjectTemplate;
|
||||||
|
@ -441,15 +469,16 @@ public class SubscriptionCanonicalizer {
|
||||||
retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
|
retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
|
||||||
retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
|
retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<org.hl7.fhir.r5.model.Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
|
|
||||||
if (topicExts.size() > 0) {
|
|
||||||
IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive();
|
|
||||||
if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) {
|
|
||||||
throw new PreconditionFailedException(Msg.code(2325) + "Topic reference must be an EventDefinition");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CanonicalTopicSubscriptionFilter convertFilter(org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent theFilter) {
|
||||||
|
CanonicalTopicSubscriptionFilter retVal = new CanonicalTopicSubscriptionFilter();
|
||||||
|
retVal.setResourceType(theFilter.getResourceType());
|
||||||
|
retVal.setFilterParameter(theFilter.getFilterParameter());
|
||||||
|
retVal.setModifier(theFilter.getModifier());
|
||||||
|
// WIP STR5 add this once it's available
|
||||||
|
// retVal.setComparator(theFilter.getComparator());
|
||||||
|
retVal.setValue(theFilter.getValue());
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -540,15 +569,6 @@ public class SubscriptionCanonicalizer {
|
||||||
retVal = ((org.hl7.fhir.r4b.model.Subscription) theSubscription).getCriteria();
|
retVal = ((org.hl7.fhir.r4b.model.Subscription) theSubscription).getCriteria();
|
||||||
break;
|
break;
|
||||||
case R5:
|
case R5:
|
||||||
org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription;
|
|
||||||
String topicElement = subscription.getTopicElement().getValue();
|
|
||||||
org.hl7.fhir.r5.model.SubscriptionTopic topic = (org.hl7.fhir.r5.model.SubscriptionTopic) subscription.getContained().stream().filter(t -> ("#" + t.getId()).equals(topicElement) || (t.getId()).equals(topicElement)).findFirst().orElse(null);
|
|
||||||
if (topic == null) {
|
|
||||||
ourLog.warn("Missing contained subscription topic in R5 subscription");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
retVal = topic.getResourceTriggerFirstRep().getQueryCriteria().getCurrent();
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException(Msg.code(2327) + "Subscription criteria is not supported for FHIR version: " + myFhirContext.getVersion().getVersion());
|
throw new IllegalStateException(Msg.code(2327) + "Subscription criteria is not supported for FHIR version: " + myFhirContext.getVersion().getVersion());
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
@JsonProperty("status")
|
@JsonProperty("status")
|
||||||
private Subscription.SubscriptionStatus myStatus;
|
private Subscription.SubscriptionStatus myStatus;
|
||||||
@JsonProperty("triggerDefinition")
|
@JsonProperty("triggerDefinition")
|
||||||
|
@Deprecated
|
||||||
private CanonicalEventDefinition myTrigger;
|
private CanonicalEventDefinition myTrigger;
|
||||||
@JsonProperty("emailDetails")
|
@JsonProperty("emailDetails")
|
||||||
private EmailDetails myEmailDetails;
|
private EmailDetails myEmailDetails;
|
||||||
|
@ -77,8 +78,12 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
private boolean myCrossPartitionEnabled;
|
private boolean myCrossPartitionEnabled;
|
||||||
@JsonProperty("sendDeleteMessages")
|
@JsonProperty("sendDeleteMessages")
|
||||||
private boolean mySendDeleteMessages;
|
private boolean mySendDeleteMessages;
|
||||||
|
@JsonProperty("isTopicSubscription")
|
||||||
private boolean myIsTopicSubscription;
|
private boolean myIsTopicSubscription;
|
||||||
|
|
||||||
|
@JsonProperty("myTopicSubscription")
|
||||||
|
private CanonicalTopicSubscription myTopicSubscription;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
|
@ -94,10 +99,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
myPayloadSearchCriteria = thePayloadSearchCriteria;
|
myPayloadSearchCriteria = thePayloadSearchCriteria;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Deprecated
|
||||||
* For now we're using the R4 TriggerDefinition, but this
|
|
||||||
* may change in the future when things stabilize
|
|
||||||
*/
|
|
||||||
public void addTrigger(CanonicalEventDefinition theTrigger) {
|
public void addTrigger(CanonicalEventDefinition theTrigger) {
|
||||||
myTrigger = theTrigger;
|
myTrigger = theTrigger;
|
||||||
}
|
}
|
||||||
|
@ -251,10 +253,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
this.myCrossPartitionEnabled = myCrossPartitionEnabled;
|
this.myCrossPartitionEnabled = myCrossPartitionEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Deprecated
|
||||||
* For now we're using the R4 triggerdefinition, but this
|
|
||||||
* may change in the future when things stabilize
|
|
||||||
*/
|
|
||||||
public CanonicalEventDefinition getTrigger() {
|
public CanonicalEventDefinition getTrigger() {
|
||||||
return myTrigger;
|
return myTrigger;
|
||||||
}
|
}
|
||||||
|
@ -291,6 +290,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
b.append(myChannelExtensions, that.myChannelExtensions);
|
b.append(myChannelExtensions, that.myChannelExtensions);
|
||||||
b.append(mySendDeleteMessages, that.mySendDeleteMessages);
|
b.append(mySendDeleteMessages, that.mySendDeleteMessages);
|
||||||
b.append(myPayloadSearchCriteria, that.myPayloadSearchCriteria);
|
b.append(myPayloadSearchCriteria, that.myPayloadSearchCriteria);
|
||||||
|
b.append(myTopicSubscription, that.myTopicSubscription);
|
||||||
return b.isEquals();
|
return b.isEquals();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,6 +361,49 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
return myIsTopicSubscription;
|
return myIsTopicSubscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PayloadString is called ContentType in R5
|
||||||
|
public String getContentType() {
|
||||||
|
assert isTopicSubscription();
|
||||||
|
return getPayloadString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CanonicalTopicSubscription getTopicSubscription() {
|
||||||
|
assert isTopicSubscription();
|
||||||
|
if (myTopicSubscription == null) {
|
||||||
|
myTopicSubscription = new CanonicalTopicSubscription();
|
||||||
|
}
|
||||||
|
return myTopicSubscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTopicSubscription(CanonicalTopicSubscription theTopicSubscription) {
|
||||||
|
myTopicSubscription = theTopicSubscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent getContent() {
|
||||||
|
assert isTopicSubscription();
|
||||||
|
return myTopicSubscription.getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTopic() {
|
||||||
|
assert isTopicSubscription();
|
||||||
|
return myTopicSubscription.getTopic();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CanonicalTopicSubscriptionFilter> getFilters() {
|
||||||
|
assert isTopicSubscription();
|
||||||
|
return myTopicSubscription.getFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeartbeatPeriod() {
|
||||||
|
assert isTopicSubscription();
|
||||||
|
return myTopicSubscription.getHeartbeatPeriod();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxCount() {
|
||||||
|
assert isTopicSubscription();
|
||||||
|
return myTopicSubscription.getMaxCount();
|
||||||
|
}
|
||||||
|
|
||||||
public static class EmailDetails implements IModelJson {
|
public static class EmailDetails implements IModelJson {
|
||||||
|
|
||||||
@JsonProperty("from")
|
@JsonProperty("from")
|
||||||
|
@ -469,11 +512,13 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public static class CanonicalEventDefinition implements IModelJson {
|
public static class CanonicalEventDefinition implements IModelJson {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public CanonicalEventDefinition() {
|
public CanonicalEventDefinition() {
|
||||||
// nothing yet
|
// nothing yet
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR Storage api
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* 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%
|
||||||
|
*/
|
||||||
|
package ca.uhn.fhir.jpa.subscription.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||||
|
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||||
|
import org.hl7.fhir.r5.model.Subscription;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class CanonicalTopicSubscription {
|
||||||
|
@JsonProperty("topic")
|
||||||
|
private String myTopic;
|
||||||
|
|
||||||
|
@JsonProperty("filters")
|
||||||
|
private List<CanonicalTopicSubscriptionFilter> myFilters;
|
||||||
|
|
||||||
|
@JsonProperty("parameters")
|
||||||
|
private Map<String, String> myParameters;
|
||||||
|
|
||||||
|
@JsonProperty("heartbeatPeriod")
|
||||||
|
private Integer myHeartbeatPeriod;
|
||||||
|
|
||||||
|
@JsonProperty("timeout")
|
||||||
|
private Integer myTimeout;
|
||||||
|
|
||||||
|
@JsonProperty("content")
|
||||||
|
private Subscription.SubscriptionPayloadContent myContent;
|
||||||
|
|
||||||
|
@JsonProperty("maxCount")
|
||||||
|
private Integer myMaxCount;
|
||||||
|
|
||||||
|
public String getTopic() {
|
||||||
|
return myTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTopic(String theTopic) {
|
||||||
|
myTopic = theTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CanonicalTopicSubscriptionFilter> getFilters() {
|
||||||
|
if (myFilters == null) {
|
||||||
|
myFilters = new ArrayList<>();
|
||||||
|
}
|
||||||
|
return myFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addFilter(CanonicalTopicSubscriptionFilter theFilter) {
|
||||||
|
getFilters().add(theFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilters(List<CanonicalTopicSubscriptionFilter> theFilters) {
|
||||||
|
myFilters = theFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getParameters() {
|
||||||
|
if (myParameters == null) {
|
||||||
|
myParameters = new HashMap<>();
|
||||||
|
}
|
||||||
|
return myParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParameters(Map<String, String> theParameters) {
|
||||||
|
myParameters = theParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getHeartbeatPeriod() {
|
||||||
|
return myHeartbeatPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeartbeatPeriod(Integer theHeartbeatPeriod) {
|
||||||
|
myHeartbeatPeriod = theHeartbeatPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTimeout() {
|
||||||
|
return myTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeout(Integer theTimeout) {
|
||||||
|
myTimeout = theTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getMaxCount() {
|
||||||
|
return myMaxCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxCount(Integer theMaxCount) {
|
||||||
|
myMaxCount = theMaxCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Subscription.SubscriptionPayloadContent getContent() {
|
||||||
|
return myContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(Subscription.SubscriptionPayloadContent theContent) {
|
||||||
|
myContent = theContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object theO) {
|
||||||
|
if (this == theO) return true;
|
||||||
|
|
||||||
|
if (theO == null || getClass() != theO.getClass()) return false;
|
||||||
|
|
||||||
|
CanonicalTopicSubscription that = (CanonicalTopicSubscription) theO;
|
||||||
|
|
||||||
|
return new EqualsBuilder().append(myTopic, that.myTopic).isEquals();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return new HashCodeBuilder(17, 37).append(myTopic).toHashCode();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR Storage api
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* 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%
|
||||||
|
*/
|
||||||
|
package ca.uhn.fhir.jpa.subscription.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.hl7.fhir.r5.model.Enumerations;
|
||||||
|
import org.hl7.fhir.r5.model.SearchParameter;
|
||||||
|
|
||||||
|
public class CanonicalTopicSubscriptionFilter {
|
||||||
|
@JsonProperty("resourceType")
|
||||||
|
String myResourceType;
|
||||||
|
|
||||||
|
@JsonProperty("filterParameter")
|
||||||
|
String myFilterParameter;
|
||||||
|
|
||||||
|
|
||||||
|
@JsonProperty("comparator")
|
||||||
|
SearchParameter.SearchComparator myComparator;
|
||||||
|
|
||||||
|
@JsonProperty("modifier")
|
||||||
|
Enumerations.SubscriptionSearchModifier myModifier;
|
||||||
|
|
||||||
|
@JsonProperty("value")
|
||||||
|
String myValue;
|
||||||
|
|
||||||
|
public String getResourceType() {
|
||||||
|
return myResourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResourceType(String theResourceType) {
|
||||||
|
myResourceType = theResourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFilterParameter() {
|
||||||
|
return myFilterParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilterParameter(String theFilterParameter) {
|
||||||
|
myFilterParameter = theFilterParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchParameter.SearchComparator getComparator() {
|
||||||
|
return myComparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setComparator(SearchParameter.SearchComparator theComparator) {
|
||||||
|
myComparator = theComparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Enumerations.SubscriptionSearchModifier getModifier() {
|
||||||
|
return myModifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModifier(Enumerations.SubscriptionSearchModifier theModifier) {
|
||||||
|
myModifier = theModifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return myValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(String theValue) {
|
||||||
|
myValue = theValue;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,20 +2,29 @@ package ca.uhn.fhir.jpa.subscription.match.registry;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscriptionFilter;
|
||||||
import ca.uhn.fhir.model.api.ExtensionDt;
|
import ca.uhn.fhir.model.api.ExtensionDt;
|
||||||
import ca.uhn.fhir.model.primitive.BooleanDt;
|
import ca.uhn.fhir.model.primitive.BooleanDt;
|
||||||
import org.hl7.fhir.r4.model.BooleanType;
|
import org.hl7.fhir.r4.model.BooleanType;
|
||||||
import org.hl7.fhir.r4.model.Extension;
|
import org.hl7.fhir.r4.model.Extension;
|
||||||
import org.hl7.fhir.r4.model.Subscription;
|
import org.hl7.fhir.r4.model.Subscription;
|
||||||
|
import org.hl7.fhir.r5.model.Coding;
|
||||||
|
import org.hl7.fhir.r5.model.Enumerations;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.rest.api.Constants.CT_FHIR_JSON_NEW;
|
||||||
import static ca.uhn.fhir.util.HapiExtensions.EX_SEND_DELETE_MESSAGES;
|
import static ca.uhn.fhir.util.HapiExtensions.EX_SEND_DELETE_MESSAGES;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
class SubscriptionCanonicalizerTest {
|
class SubscriptionCanonicalizerTest {
|
||||||
|
|
||||||
|
private static final String TEST_TOPIC = "http://test.topic";
|
||||||
FhirContext r4Context = FhirContext.forR4();
|
FhirContext r4Context = FhirContext.forR4();
|
||||||
|
|
||||||
private final SubscriptionCanonicalizer testedSC = new SubscriptionCanonicalizer(r4Context);
|
private final SubscriptionCanonicalizer testedSC = new SubscriptionCanonicalizer(r4Context);
|
||||||
|
@ -64,4 +73,62 @@ class SubscriptionCanonicalizerTest {
|
||||||
CanonicalSubscription canonicalize = dstu2Canonicalizer.canonicalize(dstu2Sub);
|
CanonicalSubscription canonicalize = dstu2Canonicalizer.canonicalize(dstu2Sub);
|
||||||
assertTrue(canonicalize.getSendDeleteMessages());
|
assertTrue(canonicalize.getSendDeleteMessages());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testR5() {
|
||||||
|
// setup
|
||||||
|
SubscriptionCanonicalizer r5Canonicalizer = new SubscriptionCanonicalizer(FhirContext.forR5());
|
||||||
|
org.hl7.fhir.r5.model.Subscription subscription = new org.hl7.fhir.r5.model.Subscription();
|
||||||
|
subscription.setStatus(Enumerations.SubscriptionStatusCodes.ACTIVE);
|
||||||
|
subscription.setContentType(CT_FHIR_JSON_NEW);
|
||||||
|
// WIP STR5 support different content types
|
||||||
|
subscription.setContent(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE);
|
||||||
|
subscription.setEndpoint("http://foo");
|
||||||
|
subscription.setTopic(TEST_TOPIC);
|
||||||
|
Coding channelType = new Coding().setSystem("http://terminology.hl7.org/CodeSystem/subscription-channel-type").setCode("rest-hook");
|
||||||
|
subscription.setChannelType(channelType);
|
||||||
|
subscription.addFilterBy(buildFilter("Observation", "param1", "value1"));
|
||||||
|
subscription.addFilterBy(buildFilter("CarePlan", "param2", "value2"));
|
||||||
|
subscription.setHeartbeatPeriod(123);
|
||||||
|
subscription.setMaxCount(456);
|
||||||
|
|
||||||
|
// execute
|
||||||
|
CanonicalSubscription canonicalize = r5Canonicalizer.canonicalize(subscription);
|
||||||
|
|
||||||
|
// verify
|
||||||
|
assertEquals(Subscription.SubscriptionStatus.ACTIVE, canonicalize.getStatus());
|
||||||
|
assertEquals(CT_FHIR_JSON_NEW, canonicalize.getContentType());
|
||||||
|
assertEquals(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE, canonicalize.getContent());
|
||||||
|
assertEquals("http://foo", canonicalize.getEndpointUrl());
|
||||||
|
assertEquals(TEST_TOPIC, canonicalize.getTopic());
|
||||||
|
assertEquals(CanonicalSubscriptionChannelType.RESTHOOK, canonicalize.getChannelType());
|
||||||
|
assertThat(canonicalize.getFilters(), hasSize(2));
|
||||||
|
|
||||||
|
CanonicalTopicSubscriptionFilter filter1 = canonicalize.getFilters().get(0);
|
||||||
|
assertEquals("Observation", filter1.getResourceType());
|
||||||
|
assertEquals("param1", filter1.getFilterParameter());
|
||||||
|
// WIP STR5 assert comparator once core libs are updated
|
||||||
|
assertEquals(Enumerations.SubscriptionSearchModifier.EQUAL, filter1.getModifier());
|
||||||
|
assertEquals("value1", filter1.getValue());
|
||||||
|
|
||||||
|
CanonicalTopicSubscriptionFilter filter2 = canonicalize.getFilters().get(1);
|
||||||
|
assertEquals("CarePlan", filter2.getResourceType());
|
||||||
|
assertEquals("param2", filter2.getFilterParameter());
|
||||||
|
// WIP STR5 assert comparator once core libs are updated
|
||||||
|
assertEquals(Enumerations.SubscriptionSearchModifier.EQUAL, filter1.getModifier());
|
||||||
|
assertEquals("value2", filter2.getValue());
|
||||||
|
assertEquals(123, canonicalize.getHeartbeatPeriod());
|
||||||
|
assertEquals(456, canonicalize.getMaxCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent buildFilter(String theResourceType, String theParam, String theValue) {
|
||||||
|
org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent filter = new org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent();
|
||||||
|
filter.setResourceType(theResourceType);
|
||||||
|
filter.setFilterParameter(theParam);
|
||||||
|
filter.setModifier(Enumerations.SubscriptionSearchModifier.EQUAL);
|
||||||
|
// WIP STR5 add comparator once core libs are updated
|
||||||
|
filter.setValue(theValue);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import java.util.stream.Collectors;
|
||||||
// This class is primarily used for testing.
|
// This class is primarily used for testing.
|
||||||
public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
|
public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(PointcutLatch.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(PointcutLatch.class);
|
||||||
|
|
||||||
private static final int DEFAULT_TIMEOUT_SECONDS = 10;
|
private static final int DEFAULT_TIMEOUT_SECONDS = 10;
|
||||||
private static final FhirObjectPrinter ourFhirObjectToStringMapper = new FhirObjectPrinter();
|
private static final FhirObjectPrinter ourFhirObjectToStringMapper = new FhirObjectPrinter();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue