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.util.ParametersUtil;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
public class BundleEntryMutator {
|
||||
|
@ -57,4 +58,9 @@ public class BundleEntryMutator {
|
|||
BaseRuntimeChildDefinition fullUrlChild = myEntryDefinition.getChildByName("fullUrl");
|
||||
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) {
|
||||
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.ResourceNotFoundException;
|
||||
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.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
@ -87,7 +88,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
|||
IClientExecutable<?, ?> operation;
|
||||
|
||||
if (theSubscription.isTopicSubscription()) {
|
||||
operation = createDeliveryRequestTopic((IBaseBundle) theMsg.getPayload(myFhirContext), theClient, thePayloadResource);
|
||||
operation = createDeliveryRequestTopic((IBaseBundle) thePayloadResource, theClient);
|
||||
} else if (isNotBlank(theSubscription.getPayloadSearchCriteria())) {
|
||||
operation = createDeliveryRequestTransaction(theSubscription, theClient, thePayloadResource);
|
||||
} else if (thePayloadType != null) {
|
||||
|
@ -141,43 +142,76 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
|||
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);
|
||||
}
|
||||
|
||||
public IBaseResource getResource(IIdType payloadId, RequestPartitionId thePartitionId, boolean theDeletedOK) throws ResourceGoneException {
|
||||
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(payloadId.getResourceType());
|
||||
public IBaseResource getResource(IIdType thePayloadId, RequestPartitionId thePartitionId, boolean theDeletedOK) throws ResourceGoneException {
|
||||
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(thePayloadId.getResourceType());
|
||||
SystemRequestDetails systemRequestDetails = new SystemRequestDetails().setRequestPartitionId(thePartitionId);
|
||||
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) {
|
||||
IBaseResource payloadResource = theMsg.getPayload(myFhirContext);
|
||||
|
||||
if (payloadResource == null || theSubscription.getRestHookDetails().isDeliverLatestVersion()) {
|
||||
IIdType payloadId = theMsg.getPayloadId(myFhirContext);
|
||||
if (payloadResource instanceof IBaseBundle) {
|
||||
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 {
|
||||
if (payloadId != null) {
|
||||
boolean deletedOK = theMsg.getOperationType() == BaseResourceModifiedMessage.OperationTypeEnum.DELETE;
|
||||
payloadResource = getResource(payloadId.toVersionless(), theMsg.getRequestPartitionId(), deletedOK);
|
||||
thePayloadResource = getResource(payloadId, theMsg.getRequestPartitionId(), deletedOK);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
IIdType resourceId = payloadResource.getIdElement();
|
||||
IIdType resourceId = thePayloadResource.getIdElement();
|
||||
if (theSubscription.getRestHookDetails().isStripVersionId()) {
|
||||
resourceId = resourceId.toVersionless();
|
||||
payloadResource.setId(resourceId);
|
||||
thePayloadResource.setId(resourceId);
|
||||
thePayloadResource.getMeta().setVersionId(null);
|
||||
}
|
||||
return payloadResource;
|
||||
return thePayloadResource;
|
||||
}
|
||||
|
||||
@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.subscription.SubscriptionChannelFactory;
|
||||
import ca.uhn.fhir.jpa.topic.SubscriptionTopicMatchingSubscriber;
|
||||
import ca.uhn.fhir.jpa.topic.SubscriptionTopicRegisteringSubscriber;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -50,6 +51,8 @@ public class MatchingQueueSubscriberLoader {
|
|||
private SubscriptionChannelFactory mySubscriptionChannelFactory;
|
||||
@Autowired
|
||||
private SubscriptionRegisteringSubscriber mySubscriptionRegisteringSubscriber;
|
||||
@Autowired(required = false)
|
||||
private SubscriptionTopicRegisteringSubscriber mySubscriptionTopicRegisteringSubscriber;
|
||||
@Autowired
|
||||
private SubscriptionActivatingSubscriber mySubscriptionActivatingSubscriber;
|
||||
@Autowired
|
||||
|
@ -70,6 +73,9 @@ public class MatchingQueueSubscriberLoader {
|
|||
ourLog.info("Starting SubscriptionTopic Matching Subscriber");
|
||||
myMatchingChannel.subscribe(mySubscriptionTopicMatchingSubscriber);
|
||||
}
|
||||
if (mySubscriptionTopicRegisteringSubscriber != null) {
|
||||
myMatchingChannel.subscribe(mySubscriptionTopicRegisteringSubscriber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,16 +19,16 @@
|
|||
*/
|
||||
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.IFhirResourceDao;
|
||||
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.SubscriptionConstants;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
|
||||
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.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.util.SubscriptionUtil;
|
||||
|
@ -49,9 +49,11 @@ import javax.annotation.Nonnull;
|
|||
* <p>
|
||||
* 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);
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
private SubscriptionCanonicalizer mySubscriptionCanonicalizer;
|
||||
|
@ -73,7 +75,7 @@ public class SubscriptionActivatingSubscriber extends BaseSubscriberForSubscript
|
|||
}
|
||||
|
||||
ResourceModifiedMessage payload = ((ResourceModifiedJsonMessage) theMessage).getPayload();
|
||||
if (!isSubscription(payload)) {
|
||||
if (!payload.hasPayloadType(myFhirContext, "Subscription")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
|
|||
private void doMatchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) {
|
||||
IIdType resourceId = theMsg.getPayloadId(myFhirContext);
|
||||
|
||||
Collection<ActiveSubscription> subscriptions = mySubscriptionRegistry.getAll();
|
||||
Collection<ActiveSubscription> subscriptions = mySubscriptionRegistry.getAllNonTopicSubscriptions();
|
||||
|
||||
ourLog.trace("Testing {} subscriptions for applicability", subscriptions.size());
|
||||
boolean anySubscriptionsMatchedResource = false;
|
||||
|
|
|
@ -23,12 +23,12 @@ 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.rest.api.server.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
|
||||
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;
|
||||
|
@ -47,7 +47,7 @@ import javax.annotation.Nonnull;
|
|||
* <p>
|
||||
* 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);
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
|
@ -74,7 +74,7 @@ public class SubscriptionRegisteringSubscriber extends BaseSubscriberForSubscrip
|
|||
|
||||
ResourceModifiedMessage payload = ((ResourceModifiedJsonMessage) theMessage).getPayload();
|
||||
|
||||
if (!isSubscription(payload)) {
|
||||
if (!payload.hasPayloadType(this.myFhirContext, "Subscription")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -85,11 +85,22 @@ class ActiveSubscriptionCache {
|
|||
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()
|
||||
.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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,8 +74,8 @@ public class SubscriptionRegistry {
|
|||
return myActiveSubscriptionCache.getAll();
|
||||
}
|
||||
|
||||
public synchronized List<ActiveSubscription> getTopicSubscriptionsByUrl(String theUrl) {
|
||||
return myActiveSubscriptionCache.getTopicSubscriptionsForUrl(theUrl);
|
||||
public synchronized List<ActiveSubscription> getTopicSubscriptionsByTopic(String theTopic) {
|
||||
return myActiveSubscriptionCache.getTopicSubscriptionsForTopic(theTopic);
|
||||
}
|
||||
|
||||
private Optional<CanonicalSubscription> hasSubscription(IIdType theId) {
|
||||
|
@ -213,4 +213,8 @@ public class SubscriptionRegistry {
|
|||
public int size() {
|
||||
return myActiveSubscriptionCache.size();
|
||||
}
|
||||
|
||||
public synchronized List<ActiveSubscription> getAllNonTopicSubscriptions() {
|
||||
return myActiveSubscriptionCache.getAllNonTopicSubscriptions();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,11 @@
|
|||
*/
|
||||
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.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.SubscriptionValidatingInterceptor;
|
||||
import ca.uhn.fhir.jpa.subscription.triggering.ISubscriptionTriggeringSvc;
|
||||
|
@ -48,6 +51,11 @@ public class SubscriptionSubmitterConfig {
|
|||
return new SubscriptionValidatingInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SubscriptionQueryValidator subscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
|
||||
return new SubscriptionQueryValidator(theDaoRegistry, theSubscriptionStrategyEvaluator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SubscriptionSubmitInterceptorLoader subscriptionMatcherInterceptorLoader() {
|
||||
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.jpa.model.entity.StorageSettings;
|
||||
import ca.uhn.fhir.jpa.topic.SubscriptionTopicValidatingInterceptor;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hl7.fhir.dstu2.model.Subscription;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -37,12 +38,15 @@ public class SubscriptionSubmitInterceptorLoader {
|
|||
private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
|
||||
@Autowired
|
||||
private SubscriptionValidatingInterceptor mySubscriptionValidatingInterceptor;
|
||||
@Autowired(required = false)
|
||||
private SubscriptionTopicValidatingInterceptor mySubscriptionTopicValidatingInterceptor;
|
||||
@Autowired
|
||||
private StorageSettings myStorageSettings;
|
||||
@Autowired
|
||||
private IInterceptorService myInterceptorRegistry;
|
||||
private boolean mySubscriptionValidatingInterceptorRegistered;
|
||||
private boolean mySubscriptionMatcherInterceptorRegistered;
|
||||
private boolean mySubscriptionTopicValidatingInterceptorRegistered;
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
|
@ -62,6 +66,11 @@ public class SubscriptionSubmitInterceptorLoader {
|
|||
myInterceptorRegistry.registerInterceptor(mySubscriptionValidatingInterceptor);
|
||||
mySubscriptionValidatingInterceptorRegistered = true;
|
||||
}
|
||||
|
||||
if (mySubscriptionTopicValidatingInterceptor != null && !mySubscriptionTopicValidatingInterceptorRegistered) {
|
||||
myInterceptorRegistry.registerInterceptor(mySubscriptionTopicValidatingInterceptor);
|
||||
mySubscriptionTopicValidatingInterceptorRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
|
@ -33,7 +33,6 @@ import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
|||
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.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.model.CanonicalSubscription;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||
|
@ -73,6 +72,8 @@ public class SubscriptionValidatingInterceptor {
|
|||
private FhirContext myFhirContext;
|
||||
@Autowired
|
||||
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
|
||||
@Autowired
|
||||
private SubscriptionQueryValidator mySubscriptionQueryValidator;
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
|
||||
public void resourcePreCreate(IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
|
||||
|
@ -146,9 +147,9 @@ public class SubscriptionValidatingInterceptor {
|
|||
if (!finished) {
|
||||
|
||||
if (subscription.isTopicSubscription()) {
|
||||
Optional<IBaseResource> oTopic = findSubscriptionTopicByUrl(subscription.getCriteriaString());
|
||||
Optional<IBaseResource> oTopic = findSubscriptionTopicByUrl(subscription.getTopic());
|
||||
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 {
|
||||
validateQuery(subscription.getCriteriaString(), "Subscription.criteria");
|
||||
|
@ -217,39 +218,7 @@ public class SubscriptionValidatingInterceptor {
|
|||
}
|
||||
|
||||
public void validateQuery(String theQuery, String theFieldName) {
|
||||
if (isBlank(theQuery)) {
|
||||
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]\"");
|
||||
}
|
||||
|
||||
mySubscriptionQueryValidator.validateCriteria(theQuery, theFieldName);
|
||||
}
|
||||
|
||||
private Optional<IBaseResource> findSubscriptionTopicByUrl(String theCriteria) {
|
||||
|
@ -332,6 +301,7 @@ public class SubscriptionValidatingInterceptor {
|
|||
@SuppressWarnings("WeakerAccess")
|
||||
public void setSubscriptionStrategyEvaluatorForUnitTest(SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
|
||||
mySubscriptionStrategyEvaluator = theSubscriptionStrategyEvaluator;
|
||||
mySubscriptionQueryValidator = new SubscriptionQueryValidator(myDaoRegistry, theSubscriptionStrategyEvaluator);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -63,4 +63,8 @@ public class ActiveSubscriptionTopicCache {
|
|||
public Collection<SubscriptionTopic> getAll() {
|
||||
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;
|
||||
|
||||
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.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;
|
||||
|
||||
public class SubscriptionTopicConfig {
|
||||
@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);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SubscriptionTopicPayloadBuilder subscriptionTopicPayloadBuilder(FhirContext theFhirContext) {
|
||||
SubscriptionTopicPayloadBuilder subscriptionTopicPayloadBuilder(FhirContext theFhirContext) {
|
||||
return new SubscriptionTopicPayloadBuilder(theFhirContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SubscriptionTopicRegistry subscriptionTopicRegistry() {
|
||||
SubscriptionTopicRegistry subscriptionTopicRegistry() {
|
||||
return new SubscriptionTopicRegistry();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SubscriptionTopicSupport subscriptionTopicSupport(FhirContext theFhirContext, DaoRegistry theDaoRegistry, SearchParamMatcher theSearchParamMatcher) {
|
||||
SubscriptionTopicSupport subscriptionTopicSupport(FhirContext theFhirContext, DaoRegistry theDaoRegistry, SearchParamMatcher theSearchParamMatcher) {
|
||||
return new SubscriptionTopicSupport(theFhirContext, theDaoRegistry, theSearchParamMatcher);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SubscriptionTopicLoader subscriptionTopicLoader() {
|
||||
SubscriptionTopicLoader 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);
|
||||
InMemoryMatchResult result = matcher.match();
|
||||
if (result.matched()) {
|
||||
// as soon as one trigger matches, we're done
|
||||
return result;
|
||||
}
|
||||
// WIP STR5 should we check the other triggers?
|
||||
}
|
||||
}
|
||||
// WIP STR5 add support for event triggers
|
||||
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.model.ResourceModifiedJsonMessage;
|
||||
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.r5.model.SubscriptionTopic;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -41,6 +42,7 @@ import org.springframework.messaging.MessagingException;
|
|||
import javax.annotation.Nonnull;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SubscriptionTopicMatchingSubscriber implements MessageHandler {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicMatchingSubscriber.class);
|
||||
|
@ -95,21 +97,23 @@ public class SubscriptionTopicMatchingSubscriber implements MessageHandler {
|
|||
SubscriptionTopicMatcher matcher = new SubscriptionTopicMatcher(mySubscriptionTopicSupport, topic);
|
||||
InMemoryMatchResult result = matcher.match(theMsg);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
IBaseResource matchedResource = theMsg.getNewPayload(myFhirContext);
|
||||
|
||||
for (ActiveSubscription activeSubscription : topicSubscriptions) {
|
||||
// WIP STR5 apply subscription filter
|
||||
IBaseResource payload = mySubscriptionTopicPayloadBuilder.buildPayload(matchedResource, theMsg, activeSubscription, topic);
|
||||
mySubscriptionMatchDeliverer.deliverPayload(payload, theMsg, activeSubscription, result);
|
||||
// WIP STR5 apply subscription filters
|
||||
IBaseBundle bundlePayload = mySubscriptionTopicPayloadBuilder.buildPayload(matchedResource, theMsg, activeSubscription, topic);
|
||||
// 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.model.ResourceModifiedMessage;
|
||||
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.r5.model.Bundle;
|
||||
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.SubscriptionTopic;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class SubscriptionTopicPayloadBuilder {
|
||||
private final FhirContext myFhirContext;
|
||||
|
||||
|
@ -39,7 +42,7 @@ public class SubscriptionTopicPayloadBuilder {
|
|||
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);
|
||||
|
||||
// WIP STR5 set eventsSinceSubscriptionStart from the database
|
||||
|
@ -48,6 +51,8 @@ public class SubscriptionTopicPayloadBuilder {
|
|||
|
||||
FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion();
|
||||
|
||||
// WIP STR5 add support for notificationShape include, revinclude
|
||||
|
||||
if (fhirVersion == FhirVersionEnum.R4B) {
|
||||
bundleBuilder.setType(Bundle.BundleType.HISTORY.toCode());
|
||||
String serializedSubscriptionStatus = FhirContext.forR5Cached().newJsonParser().encodeResourceToString(subscriptionStatus);
|
||||
|
@ -60,7 +65,8 @@ public class SubscriptionTopicPayloadBuilder {
|
|||
} else {
|
||||
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);
|
||||
switch (theMsg.getOperationType()) {
|
||||
case CREATE:
|
||||
|
@ -78,9 +84,11 @@ public class SubscriptionTopicPayloadBuilder {
|
|||
|
||||
private SubscriptionStatus buildSubscriptionStatus(IBaseResource theMatchedResource, ActiveSubscription theActiveSubscription, SubscriptionTopic theTopic, int theEventsSinceSubscriptionStart) {
|
||||
SubscriptionStatus subscriptionStatus = new SubscriptionStatus();
|
||||
subscriptionStatus.setId(UUID.randomUUID().toString());
|
||||
subscriptionStatus.setStatus(Enumerations.SubscriptionStatusCodes.ACTIVE);
|
||||
subscriptionStatus.setType(SubscriptionStatus.SubscriptionNotificationType.EVENTNOTIFICATION);
|
||||
// WIP STR5 count events since subscription start and set eventsSinceSubscriptionStart
|
||||
// store counts by subscription id
|
||||
subscriptionStatus.setEventsSinceSubscriptionStart(theEventsSinceSubscriptionStart);
|
||||
subscriptionStatus.addNotificationEvent().setEventNumber(theEventsSinceSubscriptionStart).setFocus(new Reference(theMatchedResource.getIdElement()));
|
||||
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() {
|
||||
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) {
|
||||
InMemoryMatchResult previousMatches = InMemoryMatchResult.successfulMatch();
|
||||
InMemoryMatchResult currentMatches = InMemoryMatchResult.successfulMatch();
|
||||
String previousCriteria = theQueryCriteria.getPrevious();
|
||||
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 (myOperation == ResourceModifiedMessage.OperationTypeEnum.UPDATE ||
|
||||
|
@ -85,10 +93,7 @@ public class SubscriptionTriggerMatcher {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (currentCriteria != null) {
|
||||
currentMatches = matchResource(myResource, currentCriteria);
|
||||
}
|
||||
// WIP STR5 is this the correct interpretation of requireBoth?
|
||||
// WIP STR5 implement resultForCreate and resultForDelete
|
||||
if (theQueryCriteria.getRequireBoth()) {
|
||||
return InMemoryMatchResult.and(previousMatches, currentMatches);
|
||||
} 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.match.config.SubscriptionProcessorConfig;
|
||||
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.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -49,6 +50,8 @@ public class DaoSubscriptionMatcherTest {
|
|||
private IValidationSupport myValidationSupport;
|
||||
@MockBean
|
||||
private SubscriptionChannelFactory mySubscriptionChannelFactory;
|
||||
@MockBean
|
||||
private SubscriptionQueryValidator mySubscriptionQueryValidator;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
ActiveSubscription activeSub1 = buildActiveSubscription(ID1);
|
||||
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);
|
||||
activeSubscriptionCache.put(ID2, activeSub2);
|
||||
assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL), hasSize(1));
|
||||
ActiveSubscription match = activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL).get(0);
|
||||
assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL), hasSize(1));
|
||||
ActiveSubscription match = activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL).get(0);
|
||||
assertEquals(ID2, match.getId());
|
||||
|
||||
ActiveSubscription activeSub3 = buildTopicSubscription(ID3, TEST_TOPIC_URL_OTHER);
|
||||
activeSubscriptionCache.put(ID3, activeSub3);
|
||||
assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL), hasSize(1));
|
||||
match = activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL).get(0);
|
||||
assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL), hasSize(1));
|
||||
match = activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL).get(0);
|
||||
assertEquals(ID2, match.getId());
|
||||
|
||||
assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL_OTHER), hasSize(1));
|
||||
match = activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL_OTHER).get(0);
|
||||
assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL_OTHER), hasSize(1));
|
||||
match = activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL_OTHER).get(0);
|
||||
assertEquals(ID3, match.getId());
|
||||
}
|
||||
|
||||
|
@ -136,7 +136,7 @@ public class ActiveSubscriptionCacheTest {
|
|||
private ActiveSubscription buildTopicSubscription(String theId, String theTopicUrl) {
|
||||
ActiveSubscription activeSub2 = buildActiveSubscription(theId);
|
||||
activeSub2.getSubscription().setTopicSubscription(true);
|
||||
activeSub2.getSubscription().setCriteriaString(theTopicUrl);
|
||||
activeSub2.getSubscription().getTopicSubscription().setTopic(theTopicUrl);
|
||||
return activeSub2;
|
||||
}
|
||||
|
||||
|
|
|
@ -450,7 +450,7 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
|
|||
when(myInterceptorBroadcaster.callHooks(
|
||||
eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true);
|
||||
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.getCriteria()).thenReturn(mySubscriptionCriteria);
|
||||
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);
|
||||
when(message.getPayloadId(null)).thenReturn(new IdDt("Patient", 123L));
|
||||
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.getCriteria()).thenReturn(mySubscriptionCriteria);
|
||||
when(myActiveSubscription.getId()).thenReturn("Patient/123");
|
||||
|
@ -489,7 +489,7 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
|
|||
when(myInterceptorBroadcaster.callHooks(
|
||||
eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true);
|
||||
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.getCriteria()).thenReturn(mySubscriptionCriteria);
|
||||
when(myActiveSubscription.getId()).thenReturn("Patient/123");
|
||||
|
|
|
@ -202,7 +202,7 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
mySubscriptionValidatingInterceptor.validateSubmittedSubscription(badSub, null, null, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED);
|
||||
fail();
|
||||
} 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
|
||||
|
@ -228,6 +228,11 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
SubscriptionCanonicalizer subscriptionCanonicalizer(FhirContext theFhirContext) {
|
||||
return new SubscriptionCanonicalizer(theFhirContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SubscriptionQueryValidator subscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
|
||||
return new SubscriptionQueryValidator(theDaoRegistry, theSubscriptionStrategyEvaluator);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
|
|
@ -77,8 +77,7 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
public void testReuseSubscriptionIdWithDifferentDatabaseMode() throws Exception {
|
||||
myStorageSettings.setTagStorageMode(JpaStorageSettings.TagStorageModeEnum.NON_VERSIONED);
|
||||
|
||||
String payload = "application/fhir+json";
|
||||
IdType id = createSubscription("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER", payload, null, "sub").getIdElement().toUnqualifiedVersionless();
|
||||
IdType id = createSubscription("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER", Constants.CT_FHIR_JSON_NEW, null, "sub").getIdElement().toUnqualifiedVersionless();
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
Subscription subscription = mySubscriptionDao.read(id, mySrd);
|
||||
|
@ -88,8 +87,7 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
mySubscriptionDao.delete(id, mySrd);
|
||||
waitForActivatedSubscriptionCount(0);
|
||||
|
||||
payload = "application/fhir+json";
|
||||
id = createSubscription("Observation?", payload, null, "sub").getIdElement().toUnqualifiedVersionless();
|
||||
id = createSubscription("Observation?", Constants.CT_FHIR_JSON_NEW, null, "sub").getIdElement().toUnqualifiedVersionless();
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
subscription = mySubscriptionDao.read(id, mySrd);
|
||||
|
@ -104,8 +102,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=json";
|
||||
String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=json";
|
||||
|
||||
createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||
createSubscription(criteria2, Constants.CT_FHIR_JSON_NEW);
|
||||
createSubscription(criteria1);
|
||||
createSubscription(criteria2);
|
||||
waitForActivatedSubscriptionCount(2);
|
||||
|
||||
sendObservation(code, "SNOMED-CT");
|
||||
|
@ -120,12 +118,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@Test
|
||||
public void testUpdatesHaveCorrectMetadata() throws Exception {
|
||||
String payload = "application/fhir+json";
|
||||
|
||||
String code = "1000000050";
|
||||
String criteria1 = "Observation?";
|
||||
|
||||
createSubscription(criteria1, payload);
|
||||
createSubscription(criteria1);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
/*
|
||||
|
@ -171,11 +168,9 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@Test
|
||||
public void testPlaceholderReferencesInTransactionAreResolvedCorrectly() throws Exception {
|
||||
|
||||
String payload = "application/fhir+json";
|
||||
String code = "1000000050";
|
||||
String criteria1 = "Observation?";
|
||||
createSubscription(criteria1, payload);
|
||||
createSubscription(criteria1);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
// Create a transaction that should match
|
||||
|
@ -204,12 +199,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@Test
|
||||
public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception {
|
||||
String payload = "application/fhir+json";
|
||||
|
||||
String code = "1000000050";
|
||||
String criteria1 = "Observation?";
|
||||
|
||||
createSubscription(criteria1, payload);
|
||||
createSubscription(criteria1);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
/*
|
||||
|
@ -265,12 +258,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@Test
|
||||
public void testRepeatedDeliveries() throws Exception {
|
||||
String payload = "application/fhir+json";
|
||||
|
||||
String code = "1000000050";
|
||||
String criteria1 = "Observation?";
|
||||
|
||||
createSubscription(criteria1, payload);
|
||||
createSubscription(criteria1);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
|
@ -287,12 +278,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@Test
|
||||
public void testSubscriptionRegistryLoadsSubscriptionsFromDatabase() throws Exception {
|
||||
String payload = "application/fhir+json";
|
||||
|
||||
String code = "1000000050";
|
||||
String criteria1 = "Observation?";
|
||||
|
||||
createSubscription(criteria1, payload);
|
||||
createSubscription(criteria1);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
// Manually unregister all subscriptions
|
||||
|
@ -315,8 +305,7 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
@Test
|
||||
public void testActiveSubscriptionShouldntReActivate() throws Exception {
|
||||
String criteria = "Observation?code=111111111&_format=xml";
|
||||
String payload = "application/fhir+json";
|
||||
createSubscription(criteria, payload);
|
||||
createSubscription(criteria);
|
||||
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
|
@ -327,14 +316,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@Test
|
||||
public void testRestHookSubscriptionMetaAddDoesntTriggerNewDelivery() 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);
|
||||
createSubscription(criteria1);
|
||||
createSubscription(criteria2);
|
||||
waitForActivatedSubscriptionCount(2);
|
||||
|
||||
ourLog.info("Sending an Observation");
|
||||
|
@ -381,14 +368,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
public void testRestHookSubscriptionMetaAddDoesTriggerNewDeliveryIfConfiguredToDoSo() throws Exception {
|
||||
myStorageSettings.setTriggerSubscriptionsForNonVersioningChanges(true);
|
||||
|
||||
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);
|
||||
createSubscription(criteria1);
|
||||
createSubscription(criteria2);
|
||||
waitForActivatedSubscriptionCount(2);
|
||||
|
||||
Observation obs = sendObservation(code, "SNOMED-CT");
|
||||
|
@ -431,14 +416,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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);
|
||||
createSubscription(criteria1);
|
||||
createSubscription(criteria2);
|
||||
waitForActivatedSubscriptionCount(2);
|
||||
|
||||
Observation obs = sendObservation(code, "SNOMED-CT");
|
||||
|
@ -464,13 +447,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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);
|
||||
Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
|
||||
|
@ -512,13 +493,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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);
|
||||
createSubscription(criteria1);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
myStoppableSubscriptionDeliveringRestHookSubscriber.pause();
|
||||
|
@ -553,18 +532,20 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
assertEquals("changed", observation2.getNoteFirstRep().getText());
|
||||
}
|
||||
|
||||
private void createSubscription(String criteria1) {
|
||||
createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"[*]", "[Observation]", "Observation?"})
|
||||
public void RestHookSubscriptionWithPayloadSendsDeleteRequest(String theCriteria) throws Exception {
|
||||
String payload = "application/json";
|
||||
|
||||
Extension sendDeleteMessagesExtension = new Extension()
|
||||
.setUrl(EX_SEND_DELETE_MESSAGES)
|
||||
.setValue(new BooleanType(true));
|
||||
|
||||
waitForActivatedSubscriptionCount(0);
|
||||
createSubscription(theCriteria, payload, sendDeleteMessagesExtension);
|
||||
createSubscription(theCriteria, Constants.CT_FHIR_JSON_NEW, sendDeleteMessagesExtension);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
ourLog.info("** About to send observation");
|
||||
|
@ -580,14 +561,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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 subscription = newSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||
subscription
|
||||
.getChannel()
|
||||
.addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true"));
|
||||
|
@ -629,14 +608,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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);
|
||||
Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||
Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_JSON_NEW);
|
||||
waitForActivatedSubscriptionCount(2);
|
||||
|
||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
||||
|
@ -709,14 +686,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
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);
|
||||
Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||
Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_JSON_NEW);
|
||||
waitForActivatedSubscriptionCount(2);
|
||||
|
||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
||||
|
@ -787,14 +762,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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);
|
||||
Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_XML_NEW);
|
||||
Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_XML_NEW);
|
||||
waitForActivatedSubscriptionCount(2);
|
||||
|
||||
ourLog.info("** About to send observation");
|
||||
|
@ -861,12 +834,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@Test
|
||||
public void testRestHookSubscriptionStarCriteria() throws Exception {
|
||||
String payload = "application/json";
|
||||
|
||||
String code = "1000000050";
|
||||
String criteria1 = "[*]";
|
||||
|
||||
createSubscription(criteria1, payload);
|
||||
createSubscription(criteria1);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
sendObservation(code, "SNOMED-CT");
|
||||
|
@ -887,12 +858,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@Test
|
||||
public void testRestHookSubscriptionMultiTypeCriteria() throws Exception {
|
||||
String payload = "application/json";
|
||||
|
||||
String code = "1000000050";
|
||||
String criteria1 = "[Observation,Patient]";
|
||||
|
||||
createSubscription(criteria1, payload);
|
||||
createSubscription(criteria1);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
sendOrganization();
|
||||
|
@ -915,12 +884,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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);
|
||||
createSubscription(criteria1, Constants.CT_FHIR_XML_NEW);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
ourLog.info("** About to send observation");
|
||||
|
@ -966,14 +933,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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);
|
||||
Subscription subscription2 = createSubscription(criteriaBad, Constants.CT_FHIR_XML_NEW);
|
||||
|
||||
ourLog.info("** About to send observation that wont match");
|
||||
|
||||
|
@ -1011,14 +976,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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);
|
||||
Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_XML_NEW);
|
||||
Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_XML_NEW);
|
||||
waitForActivatedSubscriptionCount(2);
|
||||
|
||||
Observation observation1 = sendObservation(code, "SNOMED-CT");
|
||||
|
@ -1032,12 +995,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@Test
|
||||
public void testRestHookSubscriptionInvalidCriteria() throws Exception {
|
||||
String payload = "application/xml";
|
||||
|
||||
String criteria1 = "Observation?codeeeee=SNOMED-CT";
|
||||
|
||||
try {
|
||||
createSubscription(criteria1, payload);
|
||||
createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||
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());
|
||||
|
@ -1046,13 +1007,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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);
|
||||
Subscription subscription = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
subscription.getChannel().addHeader("X-Foo: FOO");
|
||||
|
@ -1074,12 +1033,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
|
||||
@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);
|
||||
Subscription subscription = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
sendObservation(code, "SNOMED-CT");
|
||||
|
@ -1107,9 +1064,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
@Test
|
||||
public void testInvalidProvenanceParam() {
|
||||
assertThrows(UnprocessableEntityException.class, () -> {
|
||||
String payload = "application/fhir+json";
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
@ -1117,9 +1073,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
@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);
|
||||
Subscription subscription = newSubscription(criteriabad, Constants.CT_FHIR_JSON_NEW);
|
||||
myClient.create().resource(subscription).execute();
|
||||
});
|
||||
}
|
||||
|
@ -1127,9 +1082,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
@Test
|
||||
public void testInvalidBodySiteParam() {
|
||||
assertThrows(UnprocessableEntityException.class, () -> {
|
||||
String payload = "application/fhir+json";
|
||||
String criteriabad = "BodySite?accessType=Catheter";
|
||||
Subscription subscription = newSubscription(criteriabad, payload);
|
||||
Subscription subscription = newSubscription(criteriabad, Constants.CT_FHIR_JSON_NEW);
|
||||
myClient.create().resource(subscription).execute();
|
||||
});
|
||||
}
|
||||
|
@ -1137,9 +1091,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
@Test
|
||||
public void testGoodSubscriptionPersists() {
|
||||
assertEquals(0, subscriptionCount());
|
||||
String payload = "application/fhir+json";
|
||||
String criteriaGood = "Patient?gender=male";
|
||||
Subscription subscription = newSubscription(criteriaGood, payload);
|
||||
Subscription subscription = newSubscription(criteriaGood, Constants.CT_FHIR_JSON_NEW);
|
||||
myClient.create().resource(subscription).execute();
|
||||
await().until(() -> subscriptionCount() == 1);
|
||||
}
|
||||
|
@ -1188,9 +1141,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest {
|
|||
@Test
|
||||
public void testBadSubscriptionDoesntPersist() {
|
||||
assertEquals(0, subscriptionCount());
|
||||
String payload = "application/fhir+json";
|
||||
String criteriaBad = "BodySite?accessType=Catheter";
|
||||
Subscription subscription = newSubscription(criteriaBad, payload);
|
||||
Subscription subscription = newSubscription(criteriaBad, Constants.CT_FHIR_JSON_NEW);
|
||||
try {
|
||||
myClient.create().resource(subscription).execute();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
|
|
|
@ -175,7 +175,7 @@ public class SubscriptionTopicR4BTest extends BaseSubscriptionsR4BTest {
|
|||
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();
|
||||
retval.setUrl(SUBSCRIPTION_TOPIC_TEST_URL);
|
||||
retval.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
|
@ -188,7 +188,9 @@ public class SubscriptionTopicR4BTest extends BaseSubscriptionsR4BTest {
|
|||
queryCriteria.setPrevious("Encounter?status=" + theFrom.toCode());
|
||||
queryCriteria.setCurrent("Encounter?status=" + theCurrent.toCode());
|
||||
queryCriteria.setRequireBoth(true);
|
||||
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
|
||||
mySubscriptionTopicDao.create(retval, mySrd);
|
||||
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
package ca.uhn.fhir.jpa.subscription;
|
||||
|
||||
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.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.provider.r5.BaseResourceProviderR5Test;
|
||||
import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel;
|
||||
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.test.util.SubscriptionTestUtil;
|
||||
import ca.uhn.fhir.rest.annotation.Create;
|
||||
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
||||
import ca.uhn.fhir.rest.annotation.Update;
|
||||
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.MethodOutcome;
|
||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.test.utilities.JettyUtil;
|
||||
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.listener.SingleQueryCountHolder;
|
||||
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.IIdType;
|
||||
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.Subscription;
|
||||
import org.hl7.fhir.r5.model.SubscriptionStatus;
|
||||
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
|
@ -46,15 +49,20 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
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")
|
||||
public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test {
|
||||
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 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 SingleQueryCountHolder ourCountHolder;
|
||||
private static String ourListenerServerBase;
|
||||
|
@ -67,37 +75,28 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
|
|||
protected List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>());
|
||||
@Autowired
|
||||
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
|
||||
public void beforeRegisterRestHookListener() {
|
||||
protected void before() throws Exception {
|
||||
super.before();
|
||||
mySubscriptionTopicDao = myDaoRegistry.getResourceDao(SubscriptionTopic.class);
|
||||
mySubscriptionTestUtil.registerRestHookInterceptor();
|
||||
}
|
||||
ourListenerRestServer.unregisterProvider(mySystemProvider);
|
||||
ourListenerRestServer.registerProvider(ourTestSystemProvider);
|
||||
|
||||
@BeforeEach
|
||||
public void beforeReset() throws Exception {
|
||||
ourCreatedObservations.clear();
|
||||
ourUpdatedObservations.clear();
|
||||
ourContentTypes.clear();
|
||||
ourHeaders.clear();
|
||||
ourTestSystemProvider.clear();
|
||||
|
||||
// Delete all Subscriptions
|
||||
if (myClient != null) {
|
||||
|
@ -116,34 +115,77 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
|
|||
if (processingChannel != null) {
|
||||
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) {
|
||||
Subscription subscription = newSubscription(theCriteria, thePayload);
|
||||
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());
|
||||
|
||||
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
|
||||
protected Subscription postSubscription(Subscription subscription) {
|
||||
protected Subscription postSubscription(Subscription subscription) throws InterruptedException {
|
||||
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
|
||||
MethodOutcome methodOutcome = myClient.create().resource(subscription).execute();
|
||||
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||
|
||||
subscription.setId(methodOutcome.getId().toVersionless());
|
||||
mySubscriptionIds.add(methodOutcome.getId());
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
protected Subscription newSubscription(String theCriteria, String thePayload) {
|
||||
SubscriptionTopic topic = new SubscriptionTopic();
|
||||
topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(theCriteria);
|
||||
topic.setId("1");
|
||||
protected Subscription newTopicSubscription(String theTopicUrl, String thePayload) {
|
||||
|
||||
Subscription subscription = new Subscription();
|
||||
subscription.getContained().add(topic);
|
||||
subscription.setTopic("#1");
|
||||
subscription.setTopic(theTopicUrl);
|
||||
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()
|
||||
.setSystem(CanonicalSubscriptionChannelType.RESTHOOK.getSystem())
|
||||
|
@ -153,72 +195,56 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
|
|||
return subscription;
|
||||
}
|
||||
|
||||
|
||||
protected void waitForQueueToDrain() throws InterruptedException {
|
||||
mySubscriptionTestUtil.waitForQueueToDrain();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initializeOurCountHolder() {
|
||||
ourCountHolder = myCountHolder;
|
||||
}
|
||||
|
||||
|
||||
protected Observation sendObservation(String code, String system) {
|
||||
Observation observation = new Observation();
|
||||
CodeableConcept codeableConcept = new CodeableConcept();
|
||||
observation.setCode(codeableConcept);
|
||||
observation.getIdentifierFirstRep().setSystem("foo").setValue("1");
|
||||
Coding coding = codeableConcept.addCoding();
|
||||
coding.setCode(code);
|
||||
coding.setSystem(system);
|
||||
|
||||
observation.setStatus(Enumerations.ObservationStatus.FINAL);
|
||||
|
||||
IIdType id = myObservationDao.create(observation).getId();
|
||||
observation.setId(id);
|
||||
|
||||
return observation;
|
||||
// WIP STR5 consolidate with lambda
|
||||
protected IIdType createResource(IBaseResource theResource, boolean theExpectDelivery) throws InterruptedException {
|
||||
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
|
||||
if (theExpectDelivery) {
|
||||
mySubscriptionDeliveredLatch.setExpectedCount(1);
|
||||
}
|
||||
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
|
||||
IIdType id = dao.create(theResource, mySrd).getId();
|
||||
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||
if (theExpectDelivery) {
|
||||
mySubscriptionDeliveredLatch.awaitExpected();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
public static class ObservationListener implements IResourceProvider {
|
||||
|
||||
@Create
|
||||
public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
|
||||
ourLog.info("Received Listener Create");
|
||||
ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
|
||||
ourCreatedObservations.add(theObservation);
|
||||
extractHeaders(theRequest);
|
||||
return new MethodOutcome(new IdType("Observation/1"), true);
|
||||
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);
|
||||
|
||||
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();
|
||||
ourHeaders.add(nextName + ": " + nextValue);
|
||||
}
|
||||
}
|
||||
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||
ResourceModifiedMessage lastMessage = mySubscriptionTopicsCheckedLatch.getLatchInvocationParameterOfType(ResourceModifiedMessage.class);
|
||||
assertEquals(theResource.getIdElement().toVersionless().toString(), lastMessage.getPayloadId());
|
||||
|
||||
if (theExpectDelivery) {
|
||||
mySubscriptionDeliveredLatch.awaitExpected();
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends IBaseResource> getResourceType() {
|
||||
return Observation.class;
|
||||
protected Bundle sendTransaction(Bundle theBundle, boolean theExpectDelivery) throws InterruptedException {
|
||||
int expectedChecks = theBundle.getEntry().size();
|
||||
if (theExpectDelivery) {
|
||||
mySubscriptionDeliveredLatch.setExpectedCount(1);
|
||||
}
|
||||
|
||||
@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);
|
||||
mySubscriptionTopicsCheckedLatch.setExpectedCount(expectedChecks);
|
||||
Bundle retval = mySystemDao.transaction(mySrd, theBundle);
|
||||
mySubscriptionTopicsCheckedLatch.awaitExpected();
|
||||
if (theExpectDelivery) {
|
||||
mySubscriptionDeliveredLatch.awaitExpected();
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
|
@ -230,13 +256,45 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
|
|||
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
|
||||
public static void startListenerServer() throws Exception {
|
||||
ourListenerRestServer = new RestfulServer(FhirContext.forR5Cached());
|
||||
|
||||
ObservationListener obsListener = new ObservationListener();
|
||||
ourListenerRestServer.setResourceProviders(obsListener);
|
||||
|
||||
ourListenerServer = new Server(0);
|
||||
|
||||
ServletContextHandler proxyHandler = new ServletContextHandler();
|
||||
|
@ -257,4 +315,56 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
|
|||
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;
|
||||
|
||||
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.util.BundleUtil;
|
||||
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.SubscriptionStatus;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
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 {
|
||||
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
|
||||
public void testSubscriptionTopicRegistryBean() {
|
||||
|
@ -72,15 +37,12 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test {
|
|||
Subscription subscription = createTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL);
|
||||
waitForActivatedSubscriptionCount(1);
|
||||
|
||||
assertEquals(0, ourTestSystemProvider.getCount());
|
||||
Encounter sentEncounter = sendEncounterWithStatus(Encounter.EncounterStatus.COMPLETED);
|
||||
assertEquals(0, getSystemProviderCount());
|
||||
Encounter sentEncounter = sendEncounterWithStatus(Encounter.EncounterStatus.COMPLETED, false);
|
||||
|
||||
// Should see 1 subscription notification
|
||||
waitForQueueToDrain();
|
||||
await().until(() -> getSystemProviderCount() > 0);
|
||||
|
||||
await().until(() -> ourTestSystemProvider.getCount() > 0);
|
||||
|
||||
Bundle receivedBundle = ourTestSystemProvider.getLastInput();
|
||||
Bundle receivedBundle = getLastSystemProviderBundle();
|
||||
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, receivedBundle);
|
||||
assertEquals(2, resources.size());
|
||||
|
||||
|
@ -92,41 +54,14 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test {
|
|||
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());
|
||||
assertEquals(SUBSCRIPTION_TOPIC_TEST_URL, ss.getTopic());
|
||||
}
|
||||
|
||||
private Subscription createTopicSubscription(String theTopicUrl) {
|
||||
Subscription subscription = newSubscription(theTopicUrl, Constants.CT_FHIR_JSON_NEW);
|
||||
subscription.getMeta().addProfile(SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL);
|
||||
private Subscription createTopicSubscription(String theTopicUrl) throws InterruptedException {
|
||||
Subscription subscription = newTopicSubscription(theTopicUrl, Constants.CT_FHIR_JSON_NEW);
|
||||
return postSubscription(subscription);
|
||||
}
|
||||
|
||||
private void waitForRegisteredSubscriptionTopicCount(int theTarget) throws Exception {
|
||||
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) {
|
||||
private SubscriptionTopic createEncounterSubscriptionTopic(Encounter.EncounterStatus theFrom, Encounter.EncounterStatus theCurrent, SubscriptionTopic.InteractionTrigger... theInteractionTriggers) throws InterruptedException {
|
||||
SubscriptionTopic retval = new SubscriptionTopic();
|
||||
retval.setUrl(SUBSCRIPTION_TOPIC_TEST_URL);
|
||||
retval.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
|
@ -139,37 +74,18 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test {
|
|||
queryCriteria.setPrevious("Encounter?status=" + theFrom.toCode());
|
||||
queryCriteria.setCurrent("Encounter?status=" + theCurrent.toCode());
|
||||
queryCriteria.setRequireBoth(true);
|
||||
queryCriteria.setRequireBoth(true);
|
||||
mySubscriptionTopicDao.create(retval, mySrd);
|
||||
super.createResource(retval, false);
|
||||
return retval;
|
||||
}
|
||||
|
||||
private Encounter sendEncounterWithStatus(Encounter.EncounterStatus theStatus) {
|
||||
private Encounter sendEncounterWithStatus(Encounter.EncounterStatus theStatus, boolean theExpectDelivery) throws InterruptedException {
|
||||
Encounter encounter = new Encounter();
|
||||
encounter.setStatus(theStatus);
|
||||
|
||||
IIdType id = myEncounterDao.create(encounter, mySrd).getId();
|
||||
IIdType id = createResource(encounter, theExpectDelivery);
|
||||
encounter.setId(id);
|
||||
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.IIdType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -50,6 +51,8 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im
|
|||
protected RequestPartitionId myPartitionId;
|
||||
@JsonIgnore
|
||||
protected transient IBaseResource myPayloadDecoded;
|
||||
@JsonIgnore
|
||||
protected transient String myPayloadType;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
@ -126,6 +129,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im
|
|||
return retVal;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IBaseResource getNewPayload(FhirContext theCtx) {
|
||||
if (myPayloadDecoded == null && isNotBlank(myPayload)) {
|
||||
myPayloadDecoded = theCtx.newJsonParser().parseResource(myPayload);
|
||||
|
@ -133,6 +137,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im
|
|||
return myPayloadDecoded;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IBaseResource getPayload(FhirContext theCtx) {
|
||||
IBaseResource retVal = myPayloadDecoded;
|
||||
if (retVal == null && isNotBlank(myPayload)) {
|
||||
|
@ -143,6 +148,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im
|
|||
return retVal;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getPayloadString() {
|
||||
if (this.myPayload != null) {
|
||||
return this.myPayload;
|
||||
|
@ -228,5 +234,27 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im
|
|||
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.model.CanonicalSubscription;
|
||||
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.ExtensionDt;
|
||||
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()));
|
||||
}
|
||||
setPartitionIdOnReturnValue(theSubscription, retVal);
|
||||
retVal.setChannelType(getChannelType(subscription));
|
||||
retVal.setCriteriaString(getCriteria(theSubscription));
|
||||
retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
|
||||
retVal.setHeaders(subscription.getChannel().getHeader());
|
||||
retVal.setChannelExtensions(extractExtension(subscription));
|
||||
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) {
|
||||
String from;
|
||||
String subjectTemplate;
|
||||
|
@ -389,18 +399,10 @@ public class SubscriptionCanonicalizer {
|
|||
private CanonicalSubscription canonicalizeR5(IBaseResource 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();
|
||||
Enumerations.SubscriptionStatusCodes status = subscription.getStatus();
|
||||
if (status != null) {
|
||||
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode()));
|
||||
}
|
||||
|
||||
|
||||
setPartitionIdOnReturnValue(theSubscription, retVal);
|
||||
retVal.setChannelType(getChannelType(subscription));
|
||||
retVal.setCriteriaString(getCriteria(theSubscription));
|
||||
retVal.setEndpointUrl(subscription.getEndpoint());
|
||||
retVal.setHeaders(subscription.getHeader());
|
||||
retVal.setChannelExtensions(extractExtension(subscription));
|
||||
retVal.setIdElement(subscription.getIdElement());
|
||||
|
@ -408,14 +410,40 @@ public class SubscriptionCanonicalizer {
|
|||
retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
|
||||
retVal.setTags(extractTags(subscription));
|
||||
|
||||
|
||||
List<org.hl7.fhir.r5.model.CanonicalType> profiles = subscription.getMeta().getProfile();
|
||||
for (org.hl7.fhir.r5.model.CanonicalType next : profiles) {
|
||||
if (SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL.equals(next.getValueAsString())) {
|
||||
retVal.setTopicSubscription(true);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
// All R5 subscriptions are topic subscriptions
|
||||
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) {
|
||||
String from;
|
||||
String subjectTemplate;
|
||||
|
@ -441,15 +469,16 @@ public class SubscriptionCanonicalizer {
|
|||
retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -540,15 +569,6 @@ public class SubscriptionCanonicalizer {
|
|||
retVal = ((org.hl7.fhir.r4b.model.Subscription) theSubscription).getCriteria();
|
||||
break;
|
||||
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:
|
||||
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")
|
||||
private Subscription.SubscriptionStatus myStatus;
|
||||
@JsonProperty("triggerDefinition")
|
||||
@Deprecated
|
||||
private CanonicalEventDefinition myTrigger;
|
||||
@JsonProperty("emailDetails")
|
||||
private EmailDetails myEmailDetails;
|
||||
|
@ -77,8 +78,12 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
|||
private boolean myCrossPartitionEnabled;
|
||||
@JsonProperty("sendDeleteMessages")
|
||||
private boolean mySendDeleteMessages;
|
||||
@JsonProperty("isTopicSubscription")
|
||||
private boolean myIsTopicSubscription;
|
||||
|
||||
@JsonProperty("myTopicSubscription")
|
||||
private CanonicalTopicSubscription myTopicSubscription;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
|
@ -94,10 +99,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
|||
myPayloadSearchCriteria = thePayloadSearchCriteria;
|
||||
}
|
||||
|
||||
/**
|
||||
* For now we're using the R4 TriggerDefinition, but this
|
||||
* may change in the future when things stabilize
|
||||
*/
|
||||
@Deprecated
|
||||
public void addTrigger(CanonicalEventDefinition theTrigger) {
|
||||
myTrigger = theTrigger;
|
||||
}
|
||||
|
@ -251,10 +253,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
|||
this.myCrossPartitionEnabled = myCrossPartitionEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* For now we're using the R4 triggerdefinition, but this
|
||||
* may change in the future when things stabilize
|
||||
*/
|
||||
@Deprecated
|
||||
public CanonicalEventDefinition getTrigger() {
|
||||
return myTrigger;
|
||||
}
|
||||
|
@ -291,6 +290,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
|||
b.append(myChannelExtensions, that.myChannelExtensions);
|
||||
b.append(mySendDeleteMessages, that.mySendDeleteMessages);
|
||||
b.append(myPayloadSearchCriteria, that.myPayloadSearchCriteria);
|
||||
b.append(myTopicSubscription, that.myTopicSubscription);
|
||||
return b.isEquals();
|
||||
}
|
||||
|
||||
|
@ -361,6 +361,49 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
|||
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 {
|
||||
|
||||
@JsonProperty("from")
|
||||
|
@ -469,11 +512,13 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
|||
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static class CanonicalEventDefinition implements IModelJson {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
@Deprecated
|
||||
public CanonicalEventDefinition() {
|
||||
// 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.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.primitive.BooleanDt;
|
||||
import org.hl7.fhir.r4.model.BooleanType;
|
||||
import org.hl7.fhir.r4.model.Extension;
|
||||
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 static ca.uhn.fhir.rest.api.Constants.CT_FHIR_JSON_NEW;
|
||||
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.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class SubscriptionCanonicalizerTest {
|
||||
|
||||
private static final String TEST_TOPIC = "http://test.topic";
|
||||
FhirContext r4Context = FhirContext.forR4();
|
||||
|
||||
private final SubscriptionCanonicalizer testedSC = new SubscriptionCanonicalizer(r4Context);
|
||||
|
@ -64,4 +73,62 @@ class SubscriptionCanonicalizerTest {
|
|||
CanonicalSubscription canonicalize = dstu2Canonicalizer.canonicalize(dstu2Sub);
|
||||
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.
|
||||
public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(PointcutLatch.class);
|
||||
|
||||
private static final int DEFAULT_TIMEOUT_SECONDS = 10;
|
||||
private static final FhirObjectPrinter ourFhirObjectToStringMapper = new FhirObjectPrinter();
|
||||
|
||||
|
|
Loading…
Reference in New Issue