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:
Ken Stevens 2023-04-26 13:01:00 -04:00 committed by GitHub
parent 8813d9beda
commit 6b9af3291e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2159 additions and 1474 deletions

View File

@ -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);
}
}

View File

@ -54,4 +54,8 @@ public class ModifiableBundleEntry {
public void setFullUrl(String theFullUrl) {
myBundleEntryMutator.setFullUrl(theFullUrl);
}
public void setResource(IBaseResource theUpdatedResource) {
myBundleEntryMutator.setResource(theUpdatedResource);
}
}

View File

@ -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."

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -63,4 +63,8 @@ public class ActiveSubscriptionTopicCache {
public Collection<SubscriptionTopic> getAll() {
return myCache.values();
}
public void remove(String theSubscriptionTopicId) {
myCache.remove(theSubscriptionTopicId);
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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)));

View File

@ -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);
}
}

View File

@ -45,4 +45,8 @@ public class SubscriptionTopicRegistry {
public Collection<SubscriptionTopic> getAll() {
return myActiveSubscriptionTopicCache.getAll();
}
public void unregister(String theSubscriptionTopicId) {
myActiveSubscriptionTopicCache.remove(theSubscriptionTopicId);
}
}

View File

@ -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());
}
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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;
}

View File

@ -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");

View File

@ -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

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;
}
protected DaoMethodOutcome updateResource(IBaseResource theResource, boolean theExpectDelivery) throws InterruptedException {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
if (theExpectDelivery) {
mySubscriptionDeliveredLatch.setExpectedCount(1);
}
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
DaoMethodOutcome retval = dao.update(theResource, mySrd);
public static class ObservationListener implements IResourceProvider {
mySubscriptionTopicsCheckedLatch.awaitExpected();
ResourceModifiedMessage lastMessage = mySubscriptionTopicsCheckedLatch.getLatchInvocationParameterOfType(ResourceModifiedMessage.class);
assertEquals(theResource.getIdElement().toVersionless().toString(), lastMessage.getPayloadId());
@Create
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);
if (theExpectDelivery) {
mySubscriptionDeliveredLatch.awaitExpected();
}
return retval;
}
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);
protected Bundle sendTransaction(Bundle theBundle, boolean theExpectDelivery) throws InterruptedException {
int expectedChecks = theBundle.getEntry().size();
if (theExpectDelivery) {
mySubscriptionDeliveredLatch.setExpectedCount(1);
}
mySubscriptionTopicsCheckedLatch.setExpectedCount(expectedChecks);
Bundle retval = mySystemDao.transaction(mySrd, theBundle);
mySubscriptionTopicsCheckedLatch.awaitExpected();
if (theExpectDelivery) {
mySubscriptionDeliveredLatch.awaitExpected();
}
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return Observation.class;
}
@Update
public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
ourLog.info("Received Listener Update");
ourUpdatedObservations.add(theObservation);
ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", ""));
extractHeaders(theRequest);
return new MethodOutcome(new IdType("Observation/1"), false);
}
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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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.Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
if (topicExts.size() > 0) {
IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive();
if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) {
throw new PreconditionFailedException(Msg.code(2325) + "Topic reference must be an EventDefinition");
}
}
List<org.hl7.fhir.r5.model.CanonicalType> profiles = subscription.getMeta().getProfile();
for (org.hl7.fhir.r5.model.CanonicalType next : profiles) {
if (SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL.equals(next.getValueAsString())) {
// 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());
}

View File

@ -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
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();